分块学习笔记

分块

优雅的暴力。

分块的思想是通过划分和预处理来达到时间复杂度的平衡。

分块后任取一区间,中间会包含整块和零散块。一般对零散块暴力处理,整块通过预处理的信息计算。

常见的分块有数列分块,值域分块,数论分块等,运用于不同的情境。

分块的复杂度一般劣于线段树等 \(log\) 数据结构,但是运用范围广。

总体来说 : 划分 -> 预处理 -> 操作

数列分块

洛谷P3374 【模板】树状数组 1

维护一个长度为 \(n\) 的数列 \(a\) ,共 \(m\) 次操作 。
1 x k 将第 \(x\) 个数加上 \(k\)
2 x y 输出区间 \([x,y]\) 内每个数的和 。
\(1 \leq n,m \leq 5 \times 10^{5}\)

1.划分

可以用固定块长划分,也可以直接 \(\sqrt{n}\) 为块长。后文无说明均使用 \(\sqrt{n}\) 为块长。

此时数列被划分成 \(\sqrt{n}\) 块,需要记录每一块的左右端点,以及每个元素所属块。

注意可能最后一块是不完整的,块长不一定为 \(\sqrt{n}\)

2.预处理

因为要求和,所以我们预处理出每一块的元素和。

\(sum_{i}\) 表示第 \(i\) 块的元素和。

3.操作

两种,一个修改一个查询。

先来看修改,将第 \(x\) 个数加上 \(k\) ,同时也要将其所在块的元素和加上 \(k\)

再看查询,设 \(x\) 在第 \(p\) 块,\(y\) 在第 \(q\) 块。

\(1. \ p = q\)

表示 \(x\)\(y\) 在同一块内,\([x,y]\) 中元素个数不超过 \(\sqrt{n}\) ,可以暴力统计。

\(2. \ p \neq q\)

此时 \([x,y]\) 由如下三部分组成:

  1. 左边的散块 \(p\)
    这部分内元素个数不超过 \(\sqrt{n}\) ,直接统计求和。

  2. 中间的完整块 \(p+1 \sim q-1\)
    完整块的个数不超过 \(\sqrt{n}\) 个,枚举每个块并将其 \(sum\) 相加即可。
    注意当 \(p + 1 = q\) 时中间是没有完整块的,但是并不影响。

  3. 右边的散块 \(q\)
    这部分内元素个数不超过 \(\sqrt{n}\) ,直接统计求和。

这里散块有可能是完整的一块,不过不影响。

#include<cmath>
#include<cstdio>
const int M=5e5+10,len=800;

int n,m;

int L[len],R[len],bel[M];
int a[M],sum[len];
void build(){
    int size=sqrt(n);//块长
    for(int i=1;i<=n;i++) bel[i]=(i-1)/size+1;//计算元素所在块
    for(int i=1;i<=bel[n];i++) L[i]=(i-1)*size+1,R[i]=i*size;//每一块的左右端点
    R[bel[n]]=n;//最后一块的右端点为n
	
    for(int i=1;i<=bel[n];i++)//枚举每一块
        for(int j=L[i];j<=R[i];j++) sum[i]+=a[j];
}

void modify(int x,int k){//修改 
    a[x]+=k;
    sum[bel[x]]+=k;//所在块的和也要修改
}

int query(int l,int r){
    int p=bel[l],q=bel[r],ans=0;
    if(p==q){
        for(int i=l;i<=r;i++) ans+=a[i];
        //两端点在同一块内,直接暴力统计。
        return ans;
    }else{
        for(int i=l;i<=R[p];i++) ans+=a[i];//左边的散块
        for(int i=L[q];i<=r;i++) ans+=a[i];//右边的散块
        for(int i=p+1;i<=q-1;i++) ans+=sum[i];//中间的整块
        return ans;
    }
}

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    build();
	
    for(int i=1,opt,x,y;i<=m;i++){
        scanf("%d%d%d",&opt,&x,&y);
        if(opt==1) modify(x,y);
        if(opt==2) printf("%d\n",query(x,y));
    }
    return 0;
}

洛谷P3372 【模板】线段树 1

维护一个长度为 \(n\) 的数列 \(a\) ,共 \(m\) 次操作。
1 x y k 将区间 \([l,r]\) 的每个元素加上 \(k\)
2 x y 询问区间 \([l,r]\) 的元素和。
\(1 \leq n,m \leq 10^{5}\)

上一题的升级版。

对于操作 \(1\) 不可能一个个加,所以利用懒标记思想。整块就打上懒标记,散块就下推懒标记并暴力加。

\(l\) 在第 \(p\) 块,\(r\) 在第 \(q\) 块。

\(1. \ p = q\)

直接暴力操作即可。

\(2. \ p \neq q\)

这个时候 \([l,r]\) 由三部分组成:

  1. 左边的散块 \(p\)
    暴力操作,最多 \(\sqrt{n}\) 个元素。

  2. 中间的完整块 \(p+1 \sim q-1\)
    对其打上懒标记 \(lazy\) ,表示这一块整体还剩 \(lazy\) 没有加上去。

  3. 右边的散块 \(q\)
    暴力操作,最多 \(\sqrt{n}\) 个元素。

查询和上一题差不多,但是要记得加上 \(lazy\) 的贡献。

这题的懒标记下传不下传都行。这里写了下传的版本。下传时注意是散块的下传。

记得开 long long

void pushdown(int p){
    for(int i=L[p];i<=R[p];i++) a[i]+=lazy[p];
    sum[p]+=lazy[p]*(R[p]-L[p]+1);//维护块内和
    lazy[p]=0;//清空懒标记
}
void modify(int l,int r,int k){
    int p=bel[l],q=bel[r];
    if(p==q){
        pushdown(p);//散块下传懒标记
        for(int i=l;i<=r;i++) a[i]+=k;
        sum[p]+=(r-l+1)*k;
    }else{
        pushdown(p),pushdown(q);//处理左右散块
        for(int i=l;i<=R[p];i++) a[i]+=k;
        for(int i=L[q];i<=r;i++) a[i]+=k;
        sum[p]+=(R[p]-l+1)*k;
        sum[q]+=(r-L[q]+1)*k;

        for(int i=p+1;i<=q-1;i++) lazy[i]+=k;//整块打上懒标记
	}
}
int query(int l,int r){
    int p=bel[l],q=bel[r],ans=0;
    if(p==q){
        pushdown(p);//其实查询操作可以不用下传,但是这里下传后代码更加简洁
        for(int i=l;i<=r;i++) ans+=a[i];
    }else{
        pushdown(p),pushdown(q);
        for(int i=l;i<=R[p];i++) ans+=a[i];//左散块的和
        for(int i=L[q];i<=r;i++) ans+=a[i];//右散块的和

        for(int i=p+1;i<=q-1;i++) ans+=sum[i]+lazy[i]*(R[i]-L[i]+1);
    }
    return ans;
}

洛谷 P2801 教主的魔法

维护一个长度为 \(n\) 的数列,共 \(q\) 次操作 :
M l r w 区间 \([l,r]\) 所有元素加上 \(w\)
A l r c 询问区间 \([l,r]\) 有多少数大于等于 \(c\)
\(1 \leq n \leq 10^{6}\) , \(1 \leq q \leq 3000\)

先看查询操作。

如果在一个单调不降的数组内查询,就可以用 \(O(\log{n})\) 的二分来求出答案。

但是这个数列不一定是有序的,所以可以分块后对每一块维护一个 vector ,来存储块内元素的不降序排列。

这样散块暴力统计,整块二分查找,可以做到 \(O(n\sqrt{n}\log{n})\) 的复杂度。

那么询问解决了,接下来看如何修改,分整块和散块讨论。

整块:整块同时加上一个数 \(w\) ,其内部元素大小关系不变。所以只需要打上懒标记,不需要修改 vector

散块:部分加上一个数 \(w\) ,大小关系可能会改变。但是这一块只有 \(\sqrt{n}\) 个元素,所以下推懒标记并暴力修改,最后重构这一块的 vector 即可。也可以通过归并排序来实现 \(O(\sqrt{n})\) 重构。

但是当你将块长定为 \(\sqrt{n}\)

TLE

\(\color{Purple}TLE \ on \ \# 9\)。这里将块长设为 \(666\) ,就可以通过全部测试点。

void restruct(int p){
    v[p].clear();
    for(int i=L[p];i<=R[p];i++) v[p].push_back(a[i]);
    sort(v[p].begin(),v[p].end());
}
void pushdown(int p){
    for(int i=L[p];i<=R[p];i++) a[i]+=lazy[p];
    lazy[p]=0;
}
void build(){
    int size=666;
    for(int i=1;i<=n;i++) bel[i]=(i-1)/size+1;
    for(int i=1;i<=bel[n];i++)
        L[i]=(i-1)*size+1,R[i]=i*size;
    R[bel[n]]=n;

    for(int i=1;i<=bel[n];i++) restruct(i);
}
void update(int l,int r,int w){
    int p=bel[l],q=bel[r];
    if(p==q){
        pushdown(p);
        for(int i=l;i<=r;i++) a[i]+=w;
        restruct(p);
    }else{
        pushdown(p),pushdown(q);
        for(int i=l;i<=R[p];i++) a[i]+=w;
        for(int i=L[q];i<=r;i++) a[i]+=w;
        restruct(p),restruct(q);
        for(int i=p+1;i<=q-1;i++) lazy[i]+=w;
    }
}
int query(int l,int r,int c){
    int p=bel[l],q=bel[r],ret=0;
    if(p==q){
        for(int i=l;i<=r;i++)
            if(a[i]+lazy[p]<c) ret++;
    }else{
        for(int i=l;i<=R[p];i++)
            if(a[i]+lazy[p]<c) ret++;
        for(int i=L[q];i<=r;i++)
            if(a[i]+lazy[q]<c) ret++;
        for(int i=p+1;i<=q-1;i++)
            ret+=(lower_bound(v[i].begin(),v[i].end(),c-lazy[i])-v[i].begin());
    }
    return (r-l+1)-ret;
}

洛谷P4168 [Violet]蒲公英

给定一个长度为 \(n\) 的序列,\(m\) 次查询区间 \([l,r]\) 中最小的众数 。
\(1 \leq n \leq 40000\) , \(1 \leq m \leq 50000\) , \(1 \leq a_i \leq 10^{9}\) , 强制在线

比较好想的一道题。

\(a\) 的值域较大,先将它离散化。

\(mode_{i,j}\)\(i \sim j\) 块中的最小众数,\(pre_{i,v}\) 为前 \(i\) 块中值 \(v\) 出现的次数。二者都是可以 \(O(n\sqrt{n})\) 预处理的。

对于查询,很显然答案要么是中间整块的最小众数,要么是左右散块中的元素。

前者就是预处理的 \(mode\) 数组。后者也很好求:对于散块中某一元素 \(a\) ,其在整块 \([p+1,q-1]\) 中出现的次数为 \(pre_{q-1,a} - pre_{p,a}\) ,而在散块中出现个数可以用桶维护。最后注意清空桶。

最后前后两者取出现次数最大值即可。

int L[N],R[N],bel[M];
int pre[N][M];//值m在前n个块共有pre[n][m]个
int mode[N][N];//i~j块的众数为mode[i][j]
int buc[M];
void build(){
    int size=216;
    for(int i=1;i<=n;i++) bel[i]=(i-1)/size+1;
    for(int i=1;i<=bel[n];i++){
        L[i]=(i-1)*size+1;
        R[i]=i*size;
    }
    R[bel[n]]=n;

    for(int i=1;i<=bel[n];i++)
        for(int j=L[i];j<=R[i];j++) pre[i][a[j]]++;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=bel[n];j++) pre[j][i]+=pre[j-1][i];
    for(int i=1;i<=bel[n];i++){
        memset(buc,0,sizeof buc);
        for(int j=i;j<=bel[n];j++){
            mode[i][j]=mode[i][j-1];
            for(int k=L[j];k<=R[j];k++){
                buc[a[k]]++;
                if((buc[a[k]]>buc[mode[i][j]]) 
                or (buc[a[k]]==buc[mode[i][j]] and a[k]<mode[i][j])) mode[i][j]=a[k];
            }
        }
    }
    memset(buc,0,sizeof buc);
}
void clear(int l,int r){
    int p=bel[l],q=bel[r];
    if(p==q){
        for(int i=l;i<=r;i++) buc[a[i]]=0;
    }else{
        for(int i=l;i<=R[p];i++) buc[a[i]]=0;
        for(int j=L[q];j<=r;j++) buc[a[j]]=0;
    }
}
int query(int l,int r){
    int p=bel[l],q=bel[r];
    int ans=0,tot=0;
    if(q-p<=1){
        for(int i=l;i<=r;i++) buc[a[i]]++;
        for(int i=l;i<=r;i++)
            if(buc[a[i]]>tot or (buc[a[i]]==tot and a[i]<ans)) ans=a[i],tot=buc[a[i]];
    }else{
        for(int i=l;i<=R[p];i++){
            buc[a[i]]++;
            int tmp=pre[q-1][a[i]]-pre[p][a[i]]+buc[a[i]];
            if(tmp>tot or (tmp==tot and a[i]<ans)) ans=a[i],tot=tmp;
        }
        for(int i=L[q];i<=r;i++){
            buc[a[i]]++;
            int tmp=pre[q-1][a[i]]-pre[p][a[i]]+buc[a[i]];
            if(tmp>tot or (tmp==tot and a[i]<ans)) ans=a[i],tot=tmp;
        }
        int hzx=mode[p+1][q-1],tmp=pre[q-1][hzx]-pre[p][hzx]+buc[hzx];
        if(tmp>tot or (tmp==tot and hzx<ans)) ans=hzx,tot=tmp;
    }
    clear(l,r);
    return ans;
}

洛谷 P8576 「DTOI-2」星之界

长度为 \(n\) 的序列 \(a\)\(q\) 次操作。
1 l r x y\([l,r]\) 中所有值 \(x\) 改为 \(y\)
2 l r 输出 \(\prod_{i=l}^{r} C_{\sum_{j=l}^{i} a_j}^{a_i} \bmod 998244353\) 的值。
\(1 \leq n,q \leq 10^{5}\) , \(\sum a \leq 10^{7}\)

利用并查集打懒标记。

先看查询操作。

这么复杂的柿子不好维护,所以考虑将其化简。

\[\prod_{i=l}^{r} C_{\sum_{j=l}^{i} a_j}^{a_i} \\ = \prod_{i=l}^{r} \frac{(\sum_{j=l}^{r} a_j)!}{(\sum_{j=l}^{i-1} a_j)! \ a_i!} \\= \frac{a_l!}{1 \times a_l!} \times \frac{(a_l + a_{l+1})!}{a_l! \ a_{l+1}!} \times \frac{(a_l + a_{l+1} + a_{l+2})!}{(a_l + a_{l+1})! \ a_{l+2}!} \times \cdots \times \frac{(a_l + a_{l+1} + \cdots + a_r)!}{(a_l + a_{l+1} + \cdots + a_{r-1})! \ a_r!} \\ = \frac{(\sum_{i=l}^{r}a_i)!}{\prod_{i=l}^{r} \ (a_i!)} \]

所以需要维护块内的元素和 \(sum\) ,以及块内每个元素的阶乘逆元的积 \(frm\) 。查询区间时分母相乘,分子相加后求阶乘即可。

然后再看修改操作。显然一个个改会T飞,所以考虑能快速合并两个值的工具。

若将块内所有值为 \(x\) 的位置看成一个集合,那么很显然可以使用并查集合并。这个并查集相当于懒标记。

并查集中每个集合有一个父亲。所以设在第 \(T\) 块内值 \(x\) 第一次出现的位置 \(fir_{T,x}\) 作为其代表元,\(cnt_{T,x}\) 为块内 \(x\) 出现次数。然后后面所有的值为 \(x\) 的位置都将 \(fir_{T,x}\) 作为其父亲。顺便记录 \(rtv_i\) 为代表元 \(i\) 所代表的数。

当块 \(T\) 中将值 \(x\) 修改为 \(y\) 时:

对出现次数 \(cnt\) 的影响 : \(cnt_{T,y} \gets cnt_{T,x} + cnt_{T,y}\)

然后分类讨论:

  1. \(x\) 未出现在这一块,即 \(cnt_{T,x} = 0\)
    对这一块无影响,直接跳过即可。
  2. \(x\) 有出现,而 \(y\) 未出现,即 \(cnt_{T,x} \neq 0 \ and \ cnt_{T,y} = 0\)
    直接 \(fir_{T,y} \gets fir_{T,x}\) , \(rtv_{fir_{T,x}} \gets y\) 即可。
    表示 \(y\) 第一次出现的位置变成了原来 \(x\) 第一次出现的位置,\(fir_{T,x}\) 所代表的数变成了 \(y\)
  3. \(x,y\) 均有出现
    并查集内将 \(fir_{T,x}\) 指向 \(fir_{T,y}\) ,这样找 \(x\) 的代表元时最终会找到 \(y\) 的代表元,即 \(x\) 被修改成了 \(y\)

再考虑对于 \(sum\) , \(mul\) 的影响。设 \(x\) 在第 \(T\) 块内共出现 \(cnt_{T,x} = k\) 次,

那么 \(sum_T \gets sum_T + k \times (y - x)\) , \(frm_T \gets frm_T \times x^k \times y^{-k}\)

散块需要下传标记。a[i] = rtv[find(i)] ,就可以还原 \(a\) 的值了。

还有预处理,要计算出 \([1,10^{7}]\) 的阶乘以及其逆元,还有 \([1,10^{5}]\)\([1,\sqrt{10^{5}}]\) 次方及其逆元,这样就可以 \(O(1)\) 打标记了。

这题卡空间,所以用 int

还有个细节,修改操作中 \(x = y\) 时不能进行操作。

//dsu 为并查集
int frc[N],inv[N];//inv[x] = (x!)^(-1)
int frcpow[M][len];//frcpow[x][p] = (x!)^p
int invpow[M][len];//invpow[x][p] = (x!)^(-p)
void calc(){
    frc[1]=inv[1]=1;
    for(int i=2;i<N;i++) frc[i]=1ll*frc[i-1]*i%mod;
    for(int i=2;i<N;i++) inv[i]=1ll*inv[mod%i]*(mod-mod/i)%mod;
    for(int i=2;i<N;i++) inv[i]=1ll*inv[i]*inv[i-1]%mod;
    for(int i=1;i<M;i++){
        frcpow[i][0]=invpow[i][0]=1;
        for(int j=1;j<len;j++){
            frcpow[i][j]=1ll*frcpow[i][j-1]*frc[i]%mod;
            invpow[i][j]=1ll*invpow[i][j-1]*inv[i]%mod;
        }
    }
}

int L[len],R[len],bel[M];
int sum[len],frm[len];//sum : 区间和  frm : 区间阶乘逆元积 
int fir[len][M],rtv[M],cnt[len][M];//fir : 代表元  rtv : 代表元所代表的数   cnt : 块内计数
 
void restruct(int p){
    frm[p]=1,sum[p]=0;
    for(int i=L[p];i<=R[p];i++){
        if(!fir[p][a[i]]){
            dsu.fa[i]=fir[p][a[i]]=i;
            cnt[p][a[i]]=1;
            rtv[i]=a[i];
        }else{
            dsu.fa[i]=fir[p][a[i]];
            cnt[p][a[i]]++;
        }
        sum[p]+=a[i],frm[p]=1ll*frm[p]*inv[a[i]]%mod;
    }
}
void pushdown(int p){
    for(int i=L[p];i<=R[p];i++){
        a[i]=rtv[dsu.find(i)];
        fir[p][a[i]]=cnt[p][a[i]]=0;
    }
    for(int i=L[p];i<=R[p];i++) dsu.fa[i]=0;//这个要放在外面
}
void build(){
    int size=sqrt(n);
    for(int i=1;i<=n;i++) bel[i]=(i-1)/size+1;
    for(int i=1;i<=bel[n];i++) L[i]=(i-1)*size+1,R[i]=i*size;
    R[bel[n]]=n;
    for(int i=1;i<=bel[n];i++) restruct(i);
}
void modify(int l,int r,int x,int y){
    int p=bel[l],q=bel[r];
    if(p==q){
        pushdown(p);
        for(int i=l;i<=r;i++)
            if(a[i]==x) a[i]=y;
        restruct(p);
    }else{
        pushdown(p),pushdown(q);
        for(int i=l;i<=R[p];i++) if(a[i]==x) a[i]=y;
        for(int i=L[q];i<=r;i++) if(a[i]==x) a[i]=y;
        restruct(p),restruct(q);
		
        for(int i=p+1;i<=q-1;i++){
            int cx=cnt[i][x];
            //if(!cx) continue;
			
            sum[i]+=cx*(y-x);
            frm[i]=1ll*frm[i]*frcpow[x][cx]%mod*invpow[y][cx]%mod;//维护分母的逆元 
            cnt[i][y]+=cnt[i][x];
			
            if(!fir[i][y]) fir[i][y]=fir[i][x],rtv[fir[i][y]]=y;
            else dsu.fa[fir[i][x]]=fir[i][y];
            fir[i][x]=cnt[i][x]=0;
        }
    }
}
int query(int l,int r){
    int p=bel[l],q=bel[r];
    int tmpsum=0,tmpmul=1;
    if(p==q){
        for(int i=l;i<=r;i++){
            int cur=rtv[dsu.find(i)];
            tmpsum+=cur,tmpmul=1ll*tmpmul*inv[cur]%mod;
        }
    }else{
        for(int i=l;i<=R[p];i++){
            int cur=rtv[dsu.find(i)];
            tmpsum+=cur,tmpmul=1ll*tmpmul*inv[cur]%mod;
        }
        for(int i=L[q];i<=r;i++){
            int cur=rtv[dsu.find(i)];
            tmpsum+=cur,tmpmul=1ll*tmpmul*inv[cur]%mod;
        }
        for(int i=p+1;i<=q-1;i++){
            tmpsum+=sum[i];
            tmpmul=1ll*tmpmul*frm[i]%mod;
        }
    }
    return 1ll*frc[tmpsum]*tmpmul%mod;
}

CodeForces 896E Welcome home, Chtholly

维护一个长度为 \(n\) 的序列 \(a\) ,共 \(m\) 次操作 :
1 l r x\([l,r]\) 中所有大于 \(x\) 的元素减去 \(x\)
2 l r x 询问 \([l,r]\) 中多少个元素等于 \(x\)
\(1 \leq n,a_i \leq 10^{5}\)

第二分块,比较简单的。又叫瑟尼欧里斯树 (?)。

先看修改操作。暴力做法是直接将值域 \([x , M]\) 平移到 \([1 , M-x+1]\) 。但是当 \(x\) 很小的时候这个做法会T飞,所以当 \(x\) 很小时,选择将 \([1 , x]\) 区间平移到 \([x+1 , 2x]\) 区间(相当于小于 \(x\) 的数加上 \(x\)),然后块内打上懒标记 \(lazy\) 表示这一块的值域要减少 \(lazy\)

如何判断平移哪一段区间?设 \(Maxn\) 为这一块内元素的最大值 :

  1. \(x \ge Maxn\)
    这一块内不需要任何操作。
  2. \(x < Maxn \le 2x\)
    那么就将 \([x , Max]\) 值域平移到 \([1 , Maxn-x+1]\) ,这样在 \(O(x)\) 的时间内将值域减小了 \(x\)
  3. \(2x < Maxn\)
    那么就将 \([1 , x]\) 值域平移到 \([1+x , 2x]\) ,这样在 \(O(x)\) 的时间内将值域减小了 \(x\)

这样就保证了每块的值域都被执行了 \(O(值域)\) 次操作。

然后还需要个快速合并相同值的数据结构,也就是上一题用到的 \(并查集\)

注意 \(Maxn\) 是指块内元素减去 \(lazy\) 之前的最大值,也就是下传懒标记之前的最大值。

在操作时也要注意 \(lazy\) 对值域的影响。

struct DSU{
    int fa[M];
    int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}
    void merge(int x,int y){return fa[find(x)]=find(y),void();}
    bool query(int x,int y){return find(x)==find(y);}
}dsu; //并查集

int L[len],R[len],bel[M],lazy[len],maxn[len];
int fir[len][M],cnt[len][M],rtv[M];

void restruct(int p){
    maxn[p]=0;
    for(int i=L[p];i<=R[p];i++){
        cnt[p][a[i]]++;
        if(!fir[p][a[i]]){
            fir[p][a[i]]=dsu.fa[i]=i;
            rtv[i]=a[i];
        }else{
            dsu.fa[i]=fir[p][a[i]];
        }
        maxn[p]=max(maxn[p],a[i]);
    }
}
void pushdown(int p){
    for(int i=L[p];i<=R[p];i++){
        a[i]=rtv[dsu.find(i)];
        fir[p][a[i]]=cnt[p][a[i]]=0;
        a[i]-=lazy[p]; //注意这里要减去 lazy ,而上一行不用
    }
    for(int i=L[p];i<=R[p];i++) dsu.fa[i]=0;
    lazy[p]=0;
}
void build(){
    int size=sqrt(n);
    for(int i=1;i<=n;i++) bel[i]=(i-1)/size+1;
    for(int i=1;i<=bel[n];i++) L[i]=(i-1)*size+1,R[i]=i*size;
    R[bel[n]]=n;

    for(int i=1;i<=bel[n];i++) restruct(i);
}
void merge(int p,int x,int y){ // 第 p 块中的 x -> y
    if(!fir[p][x]) return;
    cnt[p][y]+=cnt[p][x];
    if(!fir[p][y]) fir[p][y]=fir[p][x],rtv[fir[p][x]]=y;
    else dsu.fa[fir[p][x]]=fir[p][y];
    fir[p][x]=cnt[p][x]=0;
}
void modify(int l,int r,int x){
    int p=bel[l],q=bel[r];
    if(p==q){
        pushdown(p);
        for(int i=l;i<=r;i++) if(a[i]>x) a[i]-=x;
        restruct(p);
    }else{
        pushdown(p),pushdown(q);
        for(int i=l;i<=R[p];i++) if(a[i]>x) a[i]-=x;
        for(int i=L[q];i<=r;i++) if(a[i]>x) a[i]-=x;
        restruct(p),restruct(q);

        for(int i=p+1;i<=q-1;i++){
            if(x>=maxn[i]-lazy[i]) continue;//真实的最大值是 maxn - lazy
            if(x*2<=maxn[i]-lazy[i]){
                for(int j=lazy[i]+1;j<=lazy[i]+x;j++) merge(i,j,j+x);
                lazy[i]+=x;
            }else{
                for(int j=lazy[i]+x+1;j<=maxn[i];j++) merge(i,j,j-x);
                maxn[i]=min(maxn[i],lazy[i]+x);
                //原本修改后区间最大值为 min{x , maxn[i]} 
                //但是有懒标记,所以最大值为 min{lazy[i]+x , maxn[i]}
            }
        }
    }
}
int query(int l,int r,int x){
    int p=bel[l],q=bel[r],ans=0;
    if(p==q){
        for(int i=l;i<=r;i++)
            if(rtv[dsu.find(i)]-lazy[p]==x) ans++;
    }else{
        for(int i=l;i<=R[p];i++)
            if(rtv[dsu.find(i)]-lazy[p]==x) ans++;
        for(int i=L[q];i<=r;i++)
            if(rtv[dsu.find(i)]-lazy[q]==x) ans++;
        for(int i=p+1;i<=q-1;i++)
            if(lazy[i]+x<M) ans+=cnt[i][lazy[i]+x];
    }
    return ans;
}

值域分块

和数列分块类似,但是分的是值域。值域较小的时候可以使用。

有时候其实就是数列分块,此时数列是一个桶。

比较常用于根号平衡等。一般是辅助工具。

洛谷 P1138 第 k 小整数

现有 \(n\) 个正整数,求出其中的第 \(k\) 小整数,相同的数算一次。无解输出 NO RESULT
\(n \leq 10000\) , \(k \leq 1000\) , \(正整数范围为 [1 , 30000]\)

值域很小所以可以开个桶,然后再对桶分块,块长约为 \(\sqrt{30000}\) 。记录每个块内的和。注意要去重。

然后看询问操作。先考虑如何仅用桶查询区间第 \(k\) 小,也就是找出 \(x\) ,使得 \(\sum_{i=1}^{x} buc_i = x\) 。所以一个个加起来就行。

但是这样复杂度是 \(O(值域)\) 的。用分块操作可以一次加上 \(\sqrt{值域}\) 个数。具体的,设第 \(k\) 小的数在值域第 \(p\) 块,第 \(i\) 块的和为 \(sum_i\) ,则有

\[\sum_{i}^{p-1} sum_i < k \leq \sum_{i}^{p} \]

所以一块块加起来,大于等于 \(k\) 时,就是 \(x\) 所在块,然后在块内查找。每个块其实是一个长度为 \(\sqrt{值域}\) 的桶,所以用桶的方法即可。

const int MAXN=30000;//值域
int L[len],R[len],bel[M];
int sum[len],cnt[M];
//sum 是块内和,cnt 是桶

void build(){
    int size=sqrt(MAXN); //值域分块,总长为值域
    for(int i=1;i<=MAXN;i++) bel[i]=(i-1)/size+1;
    for(int i=1;i<=bel[MAXN];i++) L[i]=(i-1)*size+1,R[i]=i*size;
    R[bel[MAXN]]=MAXN;

    for(int i=1;i<=n;i++)
        if(!cnt[a[i]]) cnt[a[i]]++,sum[bel[a[i]]]++;
        //这里要注意去重
}
int query(int k){//总体第 k 小
    int cur=0,tot=0;
    for(int i=1;i<=bel[MAXN];i++) //找到相应的块
        if(tot+sum[i]>=k){cur=i;break;}
        else tot+=sum[i];
    for(int i=L[cur];i<=R[cur];i++) //块内找到相应的数
        if(tot+cnt[i]>=k) return i;
        else tot+=cnt[i];
    return -1;
}

洛谷 P4119 [Ynoi2018] 未来日记

给定一个长度为 \(n\) 的序列 \(a\)\(m\) 次操作。
1 l r x y\([l,r]\) 区间中的 \(x\) 改为 \(y\)
2 l r k 查询 \([l,r]\) 区间的第 \(k\) 小值。
\(1 \leq n,m,a_i \leq 10^{5}\)

最初分块。

这个修改操作显然是序列分块 + 并查集。

对于查询操作,发现可以值域分块。与上一题不同的是从全局 \(k\) 小变成了区间 \(k\) 小。

所以对值域进行分块,维护两个数组 :
\(Zpre_{i,j}\) 表示序列的\(\color{CornflowerBlue}前\) \(\mathrm{i}\)中位于值域的第 \(\mathrm{j}\)的元素个数。
\(Zcnt_{i,j}\) 表示序列的\(\color{CornflowerBlue}前\) \(\mathrm{i}\)等于 \(j\)的元素个数

对于查询的散块,单独开两个数组,\(Zp_{1,i}\) 表示散块中位于值域第 \(\mathrm{i}\) 块的元素个数,\(Zp_{2,i}\) 表示散块中等于 \(i\) 的元素个数。
当查询 \([l,r]\) 区间时,中间的整块 \(\mathrm{p+1} \sim \mathrm{q-1}\) ,两边的散块 \([l,R_p] , [L_q,r]\)
\([l,r]\) 内位于值域第 \(\mathrm{i}\) 块的元素个数为 \(Zpre_{q-1,i} - Zpre_{p,i} + Zp_i\) ,等于值 \(x\) 的元素个数为 \(Zcnt_{q-1,x} - Zcnt_{p,x} + Zp2_{x}\)

然后就可以像上题一样查询了。查询完之后要清空 \(Zp\) 数组。

这时候考虑修改操作对 \(Zpre\)\(Zcnt\) 数组的影响。因为维护的是前缀和,若某一个块进行了 \(x \to y\) 修改了之后,后续每一块内 \(y\) 所属的 \(Zpre , Zcnt\) 都要加上这个块内 \(x\) 的个数,而 \(x\) 所属的则要清零。

因为这题的修改操作,每次最多多出一种数,所以数字个数是 \(O(n+m)\) 的。每次修改操作复杂度为 \(O(\sqrt{n}\alpha(n))\)\(\alpha(n)\) 是并查集的复杂度。总体时间复杂度为 \(O((n+m)\sqrt{n}\alpha(n))\),在 \(2s\) 时限下可以通过本题。块长在 \(600\) 左右时会跑得比较快。

const int MAXN=100000; //值域
struct DSU{
    int fa[M];
    int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}
}dsu;

int L[len],R[len],bel[M];//序列分块
int Zl[M],Zr[M],Zbel[M],Zpre[len][len],Zcnt[len][M];//值域分块
//Zpre[i][j] 前 i 块中值域在第 j 块的元素个数
//Zcnt[i][v] 前 i 块中值为 v 的元素个数
int fir[len][M],cnt[len][M],rtv[M];
inline void restruct(int p){
    for(register int i=L[p];i<=R[p];i++){
        cnt[p][a[i]]++;
        if(!fir[p][a[i]]){
            fir[p][a[i]]=dsu.fa[i]=i;
            rtv[i]=a[i];
        }else{
            dsu.fa[i]=fir[p][a[i]];
        }
    }
}
inline void pushdown(int p){
    for(register int i=L[p];i<=R[p];i++){
        a[i]=rtv[dsu.find(i)];
        fir[p][a[i]]=cnt[p][a[i]]=0;
    }
    for(register int i=L[p];i<=R[p];i++) dsu.fa[i]=0;
}
inline void build(){
    int size=599;
    for(register int i=1;i<=n;i++) bel[i]=(i-1)/size+1;
    for(register int i=1;i<=MAXN;i++) Zbel[i]=(i-1)/size+1;
    for(register int i=1;i<=bel[n];i++) L[i]=(i-1)*size+1,R[i]=i*size;
    for(register int i=1;i<=Zbel[MAXN];i++) Zl[i]=(i-1)*size+1,Zr[i]=i*size;
    R[bel[n]]=n,Zr[bel[MAXN]]=MAXN;

    for(register int i=1;i<=bel[n];i++){
        restruct(i);
        for(register int j=1;j<=Zbel[MAXN];j++) Zpre[i][j]=Zpre[i-1][j];
        for(register int j=1;j<=MAXN;j++) Zcnt[i][j]=Zcnt[i-1][j];
        for(register int j=L[i];j<=R[i];j++)
            Zpre[i][Zbel[a[j]]]++,Zcnt[i][a[j]]++;
    }
}
inline void merge(int p,int x,int y){
    if(!fir[p][x]) return;
    cnt[p][y]+=cnt[p][x];
    if(!fir[p][y]) fir[p][y]=fir[p][x],rtv[fir[p][x]]=y;
    else dsu.fa[fir[p][x]]=fir[p][y];
    fir[p][x]=cnt[p][x]=0;
}
inline void modify(int l,int r,int x,int y){
    if(x==y) return;
    int p=bel[l],q=bel[r];
    if(p==q){
        pushdown(p);
        int cntx=0;
        for(register int i=l;i<=r;i++)
            if(a[i]==x) a[i]=y,cntx++;
        for(register int i=p;i<=bel[n];i++){
            Zpre[i][Zbel[x]]-=cntx,Zcnt[i][x]-=cntx;
            Zpre[i][Zbel[y]]+=cntx,Zcnt[i][y]+=cntx;
        }
        restruct(p);
    }else{
        int cntx=0;//x 的个数 的前缀和
        pushdown(p);
        for(register int i=l;i<=R[p];i++)
            if(a[i]==x) cntx++,a[i]=y;
        restruct(p);
        Zpre[p][Zbel[x]]-=cntx,Zcnt[p][x]-=cntx;
        Zpre[p][Zbel[y]]+=cntx,Zcnt[p][y]+=cntx;

        for(register int i=p+1;i<=q-1;i++){
            cntx+=cnt[i][x];
            Zpre[i][Zbel[x]]-=cntx,Zcnt[i][x]-=cntx;
            Zpre[i][Zbel[y]]+=cntx,Zcnt[i][y]+=cntx;
            merge(i,x,y);
        }

        pushdown(q);
        for(register int i=L[q];i<=r;i++)
            if(a[i]==x) cntx++,a[i]=y;
        restruct(q);
        Zpre[q][Zbel[x]]-=cntx,Zcnt[q][x]-=cntx;
        Zpre[q][Zbel[y]]+=cntx,Zcnt[q][y]+=cntx;

        for(register int i=q+1;i<=bel[n];i++){
            Zpre[i][Zbel[x]]-=cntx,Zcnt[i][x]-=cntx;
            Zpre[i][Zbel[y]]+=cntx,Zcnt[i][y]+=cntx;
        }
    }
}
int Zp1[len],Zp2[M];
inline int query(int l,int r,int k){
    int p=bel[l],q=bel[r],ans=0;
    if(p==q){
        for(register int i=l;i<=r;i++)
            Zp1[Zbel[rtv[dsu.find(i)]]]++,Zp2[rtv[dsu.find(i)]]++;

        int tot=0,cur=0;
        for(register int i=1;i<=Zbel[MAXN];i++)
            if(tot+Zp1[i]>=k){cur=i;break;}
            else tot+=Zp1[i];
        for(register int i=Zl[cur];i<=Zr[cur];i++)
            if(tot+Zp2[i]>=k){ans=i;break;}
            else tot+=Zp2[i];
        
        for(register int i=l;i<=r;i++)
            Zp1[Zbel[rtv[dsu.find(i)]]]=Zp2[rtv[dsu.find(i)]]=0;
    }else{
        for(register int i=l;i<=R[p];i++)
            Zp1[Zbel[rtv[dsu.find(i)]]]++,Zp2[rtv[dsu.find(i)]]++;
        for(register int i=L[q];i<=r;i++)
            Zp1[Zbel[rtv[dsu.find(i)]]]++,Zp2[rtv[dsu.find(i)]]++;

        int tot=0,cur=0;
        for(register int i=1;i<=Zbel[MAXN];i++)
            if(tot+Zp1[i]+Zpre[q-1][i]-Zpre[p][i]>=k){cur=i;break;}
            else tot+=Zp1[i]+Zpre[q-1][i]-Zpre[p][i];
        for(register int i=Zl[cur];i<=Zr[cur];i++)
            if(tot+Zp2[i]+Zcnt[q-1][i]-Zcnt[p][i]>=k){ans=i;break;}
            else tot+=Zp2[i]+Zcnt[q-1][i]-Zcnt[p][i];

        for(register int i=l;i<=R[p];i++)
            Zp1[Zbel[rtv[dsu.find(i)]]]=Zp2[rtv[dsu.find(i)]]=0;
        for(register int i=L[q];i<=r;i++)
            Zp1[Zbel[rtv[dsu.find(i)]]]=Zp2[rtv[dsu.find(i)]]=0;
    }
    return ans;
}

但是这题改成 \(1s\) 了怎么办?

考虑到每次重构都要将散块的并查集全部推倒后重建。但是修改操作只对两种值有影响,所以可以只修改关于值 \(x,y\) 的并查集。

具体的,用一个栈记录下散块中值为 \(x\)\(y\) 的元素的位置,然后只重构并查集中这几个位置的 \(fa\) 即可。

int stk[M],top;
void update(int p,int l,int r,int x,int y){
    top=0; int tot=0;
    fir[p][x]=fir[p][y]=0;
    for(int i=L[p];i<=R[p];i++){
        a[i]=a[dsu.find(i)];
        if(a[i]==x or a[i]==y) stk[++top]=i;
    }
    for(int i=l;i<=r;i++)
        if(a[i]==x) a[i]=y,tot++;
    for(int i=1;i<=top;i++){
        if(!fir[p][y]) fir[p][y]=dsu.fa[stk[i]]=stk[i];
        else dsu.fa[stk[i]]=fir[p][y];
    }
    cnt[p][x]-=tot,cnt[p][y]+=tot;
    for(int i=p;i<=bel[n];i++){
        Zcnt[i][x]-=tot,Zcnt[i][y]+=tot;
        Zpre[i][Zbel[x]]-=tot,Zpre[i][Zbel[y]]+=tot;
    }
}
void modify(int l,int r,int x,int y){
    if(x==y) return;
    int p=bel[l],q=bel[r];
    if(p==q){
        update(p,l,r,x,y);
    }else{
        update(p,l,R[p],x,y),update(q,L[q],r,x,y);
        int sum=0;
        for(int i=p+1;i<=q-1;i++){
            sum+=cnt[i][x];
            Zpre[i][Zbel[x]]-=sum,Zpre[i][Zbel[y]]+=sum;
            Zcnt[i][x]-=sum,Zcnt[i][y]+=sum;
            merge(i,x,y);
        }
        for(int i=q;i<=bel[n];i++){
            Zpre[i][Zbel[x]]-=sum,Zpre[i][Zbel[y]]+=sum;
            Zcnt[i][x]-=sum,Zcnt[i][y]+=sum;
        }
    }
}

询问分块

对询问进行分块。一般在离线莫队里比较常用,但是也可以单独使用。

CF342E Xenia and Tree

给定一棵 \(n\) 个节点的树,一开始除了节点 \(1\) 是红色,其他节点都是黑色。支持两种操作:
1 x 将点 \(x\) 染成红色。
2 x 查询距离节点 \(x\) 最近的红点的距离。

正解是点分树。

考虑两种暴力:

1.把所有红色点丢到队列里面进行 bfs 得到所有点答案。
2.枚举所有红点,求其到某个节点 \(x\) 的距离。

然后通过分块把这两种操作串在一起。将询问分成 \(\sqrt{q}\) 块,每完成一块询问就把这块内的变色的点丢到队列里面跑 bfs 更新所有点的答案(即完成这块询问后所有点与最近的红点之间的距离),将其记为 \(ans_i\)。对于块内查询 \(u\) 点,先将答案设置为 \(ans_u\),然后和块内被修改的点的距离取 \(min\) 即可。

#include<cmath>
#include<queue>
#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
const int M=1e5+10;

int n,q,D;

vector<int>G[M],qwq;
#define add(u,v) G[u].push_back(v)
int fa[M],son[M],siz[M],dep[M],top[M];
void dfs1(int u,int f){
	fa[u]=f,siz[u]=1,dep[u]=dep[f]+1;
	for(int v:G[u])
		if(v!=f) dfs1(v,u),siz[u]+=siz[v],
			(siz[v]>siz[son[u]])?(son[u]=v):0;
}
void dfs2(int u,int t){
	top[u]=t;
	if(!son[u]) return; dfs2(son[u],t);
	for(int v:G[u])
		if(v!=fa[u] && v!=son[u]) dfs2(v,v);
}
int LCA(int u,int v){
	while(top[u]!=top[v])
		dep[top[u]]<dep[top[v]]?(v=fa[top[v]]):(u=fa[top[u]]);
	return dep[u]<dep[v]?u:v;
}
int dist(int u,int v){
	return dep[u]+dep[v]-2*dep[LCA(u,v)];
}

bool red[M]; int ans[M];
void restruct(){
	queue<int>q;
	for(int i=1;i<=n;i++) red[i]?(q.push(i),ans[i]=0):(ans[i]=-1);
	while(!q.empty()){
		int u=q.front(); q.pop();
		for(int v:G[u])
			if(ans[v]==-1) ans[v]=ans[u]+1,q.push(v);
	}
}

int main(){
	scanf("%d%d",&n,&q);
	for(int i=1,u,v;i<n;i++)
		scanf("%d%d",&u,&v),add(u,v),add(v,u);
	dfs1(1,0),dfs2(1,1);
	
	red[1]=true,D=sqrt(q);
	restruct();
	
	for(int i=1,opt,u;i<=q;i++){
		scanf("%d%d",&opt,&u);
		if(opt==1) qwq.push_back(u);
		if(opt==2){
			int res=ans[u];
			for(int v:qwq) res=min(res,dist(u,v));
			printf("%d\n",res);
		}
		if(i%D==0){
			for(int v:qwq) red[v]=true;
			restruct(),qwq.clear();
		}
	}
	return 0;
}
posted @ 2023-01-29 03:19  zzxLLL  阅读(88)  评论(0)    收藏  举报