[Ynoi2018] 五彩斑斓的世界 题解


人生第一个大分块

题意

《第二分块》

长度为 \(n\) 的序列 \(\{a\}\)\(m\) 次询问。

写一个数据结构,支持以下操作:

  1. 区间 \([l,r]\) 中大于 \(x\) 的数减去 \(x\)
  2. 区间 \([l,r]\) 中询问 \(x\) 的出现次数。

\(n \le 10^6\)\(m \le 5 \times 10^5\),\(a_i,x \le 10^5 + 1\)

分析和题解

首先可以注意到的是,值域很小。操作只会让数字变小,不会变大。

一个小小的想法就是均摊。但是这个属于说起来容易,做起来不容易的那种。不像一些比较显然的题目,想到“均摊”就能直接做的。

那么我们就得先考虑怎么利用值域实现“大于 \(x\) 的减少 \(x\)”。

基于值域的减法

我们可以维护一个并查集。

具体地,\(fa(i)\) 表示的是序列 \(\{a\}\) 中,\(a_i\) 实际上变成的元素的下标。即,如果 \(a_i\) 因为某个减法操作被修改成另一个值,恰好是 \(a_j\),那么 \(fa(i) = j\)

之后维护 \(rt(x)\) 表示 \(x\) 这个值在序列 \(\{a\}\) 中第一次出现的下标。即,如果 \(a_i\) 这个值在 \(\{a\}\) 是第一次出现,那么 \(rt(a_i) = i\)

维护一个逆于 \(rt\) 的映射 \(val\)。即,\(val(rt(a_i)) = a_i\)

维护 \(sz(x)\) 表示 \(x\) 这个值的出现次数。

这样,我们可以对于所有是同一个值 \(x\) 的元素都直接并进 \(fa(rt(x))\)

设我们把值为 \(x\) 的合并到值为 \(y\) 的上。

  • 如果 \(rt(y)\) 不存在,\(rt(y) = rt(x), val(rt(y)) = y\)
  • 否则,\(fa(rt(x)) = rt(y)\)
  • 不论如何,都计算 \(sz(y) += sz(x)\),并清空关于 \(x\) 的信息:\(sz(x) = rt(x) = 0\)

模拟一下:

实现了这一部分之后,我们就可以基于值域进行一个减法运算了。

比如,给 \(k\) 减去 \(x\),相当于是把 \(k\) 合并到 \(k-x\)

比如,大于 \(x\) 的减去 \(x\),相当于是把 \([x + 1 ,maxv]\) 合并到 \([1,maxv - x]\)

怎么均摊比较合适

我们可以看到,如果只是朴素实现刚才的功能的话,我们花费 \(O(maxv - x)\) 的时间,让值域缩小 \(x\)

但是,如果每次修改的 \(x\) 均为 \(1\),那么这种方法就会被卡死。

所以我们需要一个能在花费一定数量时间,就必然缩小至少是相当范围值域的均摊的做法。

人类智慧:操作 \(1\) 相当于是小于等于 \(x\) 的加 \(x\),再区间减 \(x\)。而区间减相当于对于这个区间整体平移,不会影响到这个区间的极差,也就是说,不会影响值域“范围”。可以打减法标记实现。

我们设块内最大值为 \(maxv\),分类讨论:

  • \(maxv > 2 \times x\),这个时候我们可以使用人类智慧,把 \([1,x]\) 合并到 \([1 + x,2 \times x]\)
  • \(maxv \le 2 \times x\),这个时候我们使用原来的方法,把 \([x + 1,maxv]\) 合并到 \([1,maxv - x]\)

也就是说:

  • \(maxv > 2 \times x\),花费 \(O(x) \times O(\text{ds})\) 的时间把范围缩小 \(O(x)\)
  • \(maxv \le 2 \times x\),花费 \(O(maxv - x) \times O(\text{ds})\) 的时间把范围缩小 \(O(maxv - x)\)

这样就可以均摊了。最多是 \(10^5 \times O(\text{ds})\)

空间

\(\text{lxl}\) 卡了空间。

由于答案可加,我们可以用询问离线化的技巧。

枚举每个块,对于每个块枚举询问,算出这个块对于某一次询问的贡献,存到答案里面。

这样,下一个块就可以接着利用上一个块的空间。

代码

#include 
#define DEBUG puts("QAQ")
#define openFile(a) freopen(a".in","r",stdin),freopen(a".out","w",stdout)
#define NOSYNC ios::sync_with_stdio(false); cin.tie(0); cout.tie(0)
#define FOR(i,j,k)  for(int (i) = (j); (i) <= (k); ++ (i))
#define RFOR(i,j,k) for(int (i) = (j); (i) >= (k); -- (i))
#define For(i,j,k)  for(int (i) = (j); (i) < (k); ++ (i))
#define RFor(i,j,k) for(int (i) = (j); (i) > (k); -- (i))
#define SC(...) scanf(__VA_ARGS__)
#define PR(...) printf(__VA_ARGS__)
#define max(a,b) (a > b ? a : b)
using namespace std;
const int N = 1e6 + 5;
const int M = 5e5 + 5;
const int V = 1e5 + 5;
const int SQN = 1e3 + 5;
struct query {
    int op, ql, qr, qx;
}Q[M]; int ans[M];
int n, m, a[N]; int B;
int fa[N], sz[V], rt[V], val[N];
int L[SQN], R[SQN]; //int bel[N];
int maxv, tag;
//-----------------------------------------------
inline int find(int x)
{
    if(x == fa[x]) return x;
    return fa[x] = find(fa[x]);
}
inline void merge(int x, int y)
{
    if(!rt[y]) rt[y] = rt[x], val[rt[y]] = y;
    else fa[rt[x]] = rt[y];
    sz[y] += sz[x];
    sz[x] = rt[x] = 0;
    /* 并查集的合并,把值为 x 的合并到值为 y 的
     * fa[i] 表示 a[i] 实际的元素在 {a} 的下标
     * rt[x] 表示 x 这个值在 {a} 中第一次出现的下标,即rt[a[i]] = i;
     * val[i] 表示 i 这个下标的值,即 val[rt[a[i]]] = a[i];
     * sz[x] 表示 x 这个值的出现次数
     */
}
//-----------------------------------------------
inline void build(int x)
{
    maxv = tag = 0;
    FOR(i,L[x],R[x])
    {
        maxv = max(maxv, a[i]);
        if(rt[a[i]]) fa[i] = rt[a[i]];
        else rt[a[i]] = fa[i] = i, val[i] = a[i];
        ++ sz[a[i]];
    }
    /* 构建一个整块
     * 主要是并查集维护
     */
}

inline void rebuild(int x, int l, int r, int k)
{
    FOR(i,L[x],R[x])
    {
        a[i] = val[find(i)];
        rt[a[i]] = sz[a[i]] = 0;
        a[i] -= tag;
    }
    FOR(i,L[x],R[x]) val[i] = 0;
    l = max(l, L[x]); r = min(r, R[x]);
    FOR(i,l,r) if(a[i] > k) a[i] -= k;
    build(x);
    /* 对散块 x 进行重构,只重构大区间 [l,r] 和散块 x 的重合部分
     * 主要是根据并查集还原了原序列的值,
     * 清空并查集对应部分维护的数据
     * 减去减法标记
     * 之后暴力进行一次操作 1
     */
}
inline void modify(int x)
{
    if(maxv - tag <= 2 * x)
    {
        FOR(i,tag + x + 1,maxv) 
            if(rt[i]) merge(i,i - x);
        maxv = min(maxv, tag + x);
    }
    /* maxv - tag 获取真实的 maxv, maxv <= 2x 时
     * 我们缩小值域,把所有的比 x 大的都减去 x
     * 这里由于有减法标记所以 [x + 1,maxv] 变成 [tag + x + 1,maxv]
     */
    else
    {
        FOR(i,tag + 1,tag + x)
            if(rt[i]) merge(i, i + x);
        tag += x;
    }
    /* maxv > 2x 时选择转化
     * 把所有 <= x 的都加上 x,再打一个减法标记
     * 这里由于有减法标记所以事实上 [1,x] 变成 [1 + tag,x + tag]
     */
}
inline void query(int x, int p)
{
    int l = Q[p].ql, r = Q[p].qr;
    if(tag + Q[p].qx > 1e5 + 1) return;
    if(l <= L[x] && r >= R[x]) ans[p] += sz[tag + Q[p].qx];
    else
    {
        l = max(l, L[x]), r = min(r, R[x]);
        FOR(i,l,r) if(val[find(i)] == tag + Q[p].qx) ++ ans[p]; 
    }
    /* tag + qx > 1e5 + 1,相当于询问的 x 超过了值域,不可能有值与其相等。
     * 如果询问的 l <= L[x] <= R[x] <= r 那么这个整块可以直接算贡献
     * 否则就暴力加个数
     */
}
int main()
{
	SC("%d%d",&n,&m);
	FOR(i,1,n) SC("%d",&a[i]);
	FOR(i,1,m) SC("%d%d%d%d",&Q[i].op,&Q[i].ql,&Q[i].qr,&Q[i].qx);
	B = sqrt(n);
	FOR(i,1,B) 
	{
	    L[i] = (i - 1) * B + 1;
	    R[i] = i * B;
	}
	R[B] = n;
	//FOR(i,1,B) FOR(j,L[i],R[i]) bel[j] = i;
	//
	FOR(i,1,B)
	{
	    memset(rt, 0, sizeof(rt));
	    memset(sz, 0, sizeof(sz));
	    build(i);
	    FOR(j,1,m)
	    {
	        if(L[i] > Q[j].qr || R[i] < Q[j].ql) continue;
	        if(Q[j].op == 1)
	        {
	            if(Q[j].ql <= L[i] && Q[j].qr >= R[i]) modify(Q[j].qx);
	            else rebuild(i, Q[j].ql, Q[j].qr, Q[j].qx);
	        }
	        else if(Q[j].op == 2) query(i, j);
	    }
	}
	/* 对于每一块分别计算,复用内存,答案累加
	 * 按顺序枚举询问,看询问是否可以整块处理,不能整块处理就重构
	 */
	FOR(i,1,m) if(Q[i].op == 2) PR("%d\n",ans[i]);
	return 0;
}