分块学习笔记
分块
优雅的暴力。
分块的思想是通过划分和预处理来达到时间复杂度的平衡。
分块后任取一区间,中间会包含整块和零散块。一般对零散块暴力处理,整块通过预处理的信息计算。
常见的分块有数列分块,值域分块,数论分块等,运用于不同的情境。
分块的复杂度一般劣于线段树等 \(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]\) 由如下三部分组成:
-
左边的散块 \(p\)
这部分内元素个数不超过 \(\sqrt{n}\) ,直接统计求和。 -
中间的完整块 \(p+1 \sim q-1\)
完整块的个数不超过 \(\sqrt{n}\) 个,枚举每个块并将其 \(sum\) 相加即可。
注意当 \(p + 1 = q\) 时中间是没有完整块的,但是并不影响。 -
右边的散块 \(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]\) 由三部分组成:
-
左边的散块 \(p\)
暴力操作,最多 \(\sqrt{n}\) 个元素。 -
中间的完整块 \(p+1 \sim q-1\)
对其打上懒标记 \(lazy\) ,表示这一块整体还剩 \(lazy\) 没有加上去。 -
右边的散块 \(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}\) 后

会 \(\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}\) 。
利用并查集打懒标记。
先看查询操作。
这么复杂的柿子不好维护,所以考虑将其化简。
所以需要维护块内的元素和 \(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}\) 。
然后分类讨论:
- \(x\) 未出现在这一块,即 \(cnt_{T,x} = 0\)
对这一块无影响,直接跳过即可。 - \(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\) 。 - \(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\) 为这一块内元素的最大值 :
- \(x \ge Maxn\)
这一块内不需要任何操作。 - \(x < Maxn \le 2x\)
那么就将 \([x , Max]\) 值域平移到 \([1 , Maxn-x+1]\) ,这样在 \(O(x)\) 的时间内将值域减小了 \(x\) 。 - \(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\) ,则有
所以一块块加起来,大于等于 \(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;
}

浙公网安备 33010602011771号