分块学习笔记

分块

优雅的暴力。

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

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

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

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

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

数列分块#

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

维护一个长度为 n 的数列 a ,共 m 次操作 。
1 x k 将第 x 个数加上 k
2 x y 输出区间 [x,y] 内每个数的和 。
1n,m5×105

1.划分

可以用固定块长划分,也可以直接 n 为块长。后文无说明均使用 n 为块长。

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

注意可能最后一块是不完整的,块长不一定为 n

2.预处理

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

sumi 表示第 i 块的元素和。

3.操作

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

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

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

1. p=q

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

2. pq

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

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

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

  3. 右边的散块 q
    这部分内元素个数不超过 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] 的元素和。
1n,m105

上一题的升级版。

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

l 在第 p 块,r 在第 q 块。

1. p=q

直接暴力操作即可。

2. pq

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

  1. 左边的散块 p
    暴力操作,最多 n 个元素。

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

  3. 右边的散块 q
    暴力操作,最多 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
1n106 , 1q3000

先看查询操作。

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

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

这样散块暴力统计,整块二分查找,可以做到 O(nnlogn) 的复杂度。

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

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

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

但是当你将块长定为 n

TLE

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] 中最小的众数 。
1n40000 , 1m50000 , 1ai109 , 强制在线

比较好想的一道题。

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

modei,jij 块中的最小众数,prei,v 为前 i 块中值 v 出现的次数。二者都是可以 O(nn) 预处理的。

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

前者就是预处理的 mode 数组。后者也很好求:对于散块中某一元素 a ,其在整块 [p+1,q1] 中出现的次数为 preq1,aprep,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 的序列 aq 次操作。
1 l r x y[l,r] 中所有值 x 改为 y
2 l r 输出 i=lrCj=liajaimod998244353 的值。
1n,q105 , a107

利用并查集打懒标记。

先看查询操作。

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

i=lrCj=liajai=i=lr(j=lraj)!(j=li1aj)! ai!=al!1×al!×(al+al+1)!al! al+1!×(al+al+1+al+2)!(al+al+1)! al+2!××(al+al+1++ar)!(al+al+1++ar1)! ar!=(i=lrai)!i=lr (ai!)

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

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

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

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

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

对出现次数 cnt 的影响 : cntT,ycntT,x+cntT,y

然后分类讨论:

  1. x 未出现在这一块,即 cntT,x=0
    对这一块无影响,直接跳过即可。
  2. x 有出现,而 y 未出现,即 cntT,x0 and cntT,y=0
    直接 firT,yfirT,x , rtvfirT,xy 即可。
    表示 y 第一次出现的位置变成了原来 x 第一次出现的位置,firT,x 所代表的数变成了 y
  3. x,y 均有出现
    并查集内将 firT,x 指向 firT,y ,这样找 x 的代表元时最终会找到 y 的代表元,即 x 被修改成了 y

再考虑对于 sum , mul 的影响。设 x 在第 T 块内共出现 cntT,x=k 次,

那么 sumTsumT+k×(yx) , frmTfrmT×xk×yk

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

还有预处理,要计算出 [1,107] 的阶乘以及其逆元,还有 [1,105][1,105] 次方及其逆元,这样就可以 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
1n,ai105

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

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

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

  1. xMaxn
    这一块内不需要任何操作。
  2. x<Maxn2x
    那么就将 [x,Max] 值域平移到 [1,Maxnx+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
n10000 , k1000 , [1,30000]

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

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

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

ip1sumi<kip

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

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 的序列 am 次操作。
1 l r x y[l,r] 区间中的 x 改为 y
2 l r k 查询 [l,r] 区间的第 k 小值。
1n,m,ai105

最初分块。

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

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

所以对值域进行分块,维护两个数组 :
Zprei,j 表示序列的 i中位于值域的第 j的元素个数。
Zcnti,j 表示序列的 i等于 j的元素个数

对于查询的散块,单独开两个数组,Zp1,i 表示散块中位于值域第 i 块的元素个数,Zp2,i 表示散块中等于 i 的元素个数。
当查询 [l,r] 区间时,中间的整块 p+1q1 ,两边的散块 [l,Rp],[Lq,r]
[l,r] 内位于值域第 i 块的元素个数为 Zpreq1,iZprep,i+Zpi ,等于值 x 的元素个数为 Zcntq1,xZcntp,x+Zp2x

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

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

因为这题的修改操作,每次最多多出一种数,所以数字个数是 O(n+m) 的。每次修改操作复杂度为 O(nα(n))α(n) 是并查集的复杂度。总体时间复杂度为 O((n+m)nα(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 的并查集。

具体的,用一个栈记录下散块中值为 xy 的元素的位置,然后只重构并查集中这几个位置的 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 的距离。

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

作者:zzxLLL

出处:https://www.cnblogs.com/zzxLLL/p/17071636.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   zzxLLL  阅读(77)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示