分块与莫队
不沾树的博客变短了好多。
分块
这道题显然可以使用线段树乱搞过去,不过为了给主角面子我们假设我们不会做。
对于一些难以使用数据结构维护答案的序列问题,我们考虑暴力。但是暴力太慢了,于是人们提出了分块。
分块,就是把序列分成许多的小段,通过一些神秘的处理实现优化暴力。
并且应当保证,整块内的答案可以 \(O(1)\) 获取,非整块的答案暴力获得后得以与块内答案处理。
考虑均值不等式,当且仅当块数 \(tot\) 与块长 \(num\) 都为 \(\sqrt{n}\) 时。这个暴力能被优化到 \(n\sqrt n\)。
考虑用分块解决线段树问题。\(O(n\sqrt n)\) 肯定不如 \(O(n\ logn)\) 优秀,但是大多数情况这俩都能过。
分块的区间操作也参考了线段树的 \(lazytag\)。使用 \(flag_i\) 表示第 \(i\) 个块的整体加数,边角就暴力改。\(sum_i\) 为第 \(i\) 个块的元素和,按照线段树的写法写就好了。
有一个技巧:不建议在分块中使用下放上传操作,而是汇总答案对每个部分都加上 \(flag\)。
#include<bits/stdc++.h>
#define int long long
#define MAXN 100005
#define MAXM 320
using namespace std;
int n,q;
int num,tot,f[MAXN],siz[MAXM];
int a[MAXN],flag[MAXM],sum[MAXM];
inline void modify(int l,int r,int val){
for(int i=l;i<=min(r,f[l]*num);i++)a[i]+=val,sum[f[i]]+=val;
if(f[l]!=f[r]){
for(int i=(f[r]-1)*num+1;i<=r;i++)a[i]+=val,sum[f[i]]+=val;
}
for(int i=f[l]+1;i<=f[r]-1;i++)flag[i]+=val;
}
inline int getans(int l,int r){
int res=0;
for(int i=l;i<=min(r,f[l]*num);i++)res+=a[i]+flag[f[i]];
if(f[l]!=f[r]){
for(int i=(f[r]-1)*num+1;i<=r;i++)res+=a[i]+flag[f[i]];
}
for(int i=f[l]+1;i<=f[r]-1;i++)res+=sum[i]+flag[i]*siz[i];
return res;
}
signed main(){
scanf("%lld%lld",&n,&q);
num=sqrt(n);
tot=(n+num-1)/num;
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
for(int i=1;i<=n;i++)f[i]=(i-1)/num+1,sum[f[i]]+=a[i],++siz[f[i]];
for(int id=1,opt,l,r,k;id<=q;id++){
scanf("%lld%lld%lld",&opt,&l,&r);
if(opt==1){
scanf("%lld",&k);
modify(l,r,k);
}
else printf("%lld\n",getans(l,r));
}
return 0;
}
在数据随机的情况下,分块可能会体现更优秀的时间复杂度。
分块的思想简单粗暴,但是如何有效地设计并处理分块来有效降低复杂度与其他技巧的兼容性极强,并不好想,这导致分块题目难度上限高。
这道题用动态树解决,但是我们不会动态树,使用分块解决。
考虑将序列分成 \(\sqrt n\) 块,\(jump_{i},to_{i}\) 分别表示在第 \(i\) 个位置放置羊,它跳出 \(i\) 所在块的步数与跳出后的位置。
这样统计答案就是 \(O(\sqrt n)\) 了。由于是单点修改 \(loc\) 的权值,只需要重置 \(loc\) 所在块的信息即可。具体地,从 \(loc\) 开始往回扫块:
这样修改也是 \(O(\sqrt n)\)。
如何处理块内有多少元素大于 \(val\)?先不去想块的问题,假如只有一个序列,如何处理序列中多少元素大于 \(val\)?显然有排序+二分的 \(O(nlog\ n)\) 做法。于是考虑将每个块排序,在整块内二分,在边角上暴力。查询复杂度 \(O(\sqrt n+\sqrt nlog\sqrt n)\)。
考虑修改,修改操作只会对边角所在的块造成影响,需要暴力重造两个块,复杂度 \(O(\sqrt n+\sqrt nlog\sqrt n)\)。
总复杂度 \(O(Q(\sqrt n+\sqrt nlog\sqrt n))\)。非常的抽象。
注意最后的块可能是不完整的,我们不想处理块的大小导致的巨量细节,使用 vector 能处理地稍微方便一些。
#include<bits/stdc++.h>
#define MAXN 1000005
#define MAXM 1005
#define int long long
using namespace std;
int n,q;
int num,tot,f[MAXN];
vector<int>val[MAXM];
int a[MAXN],block[MAXM];
inline void reset(int id){
val[id].clear();
for(int i=(id-1)*num+1;i<=min(n,id*num);i++)val[id].push_back(a[i]);
sort(val[id].begin(),val[id].end());
}
signed main(){
scanf("%lld%lld",&n,&q);
num=sqrt(n);
tot=(n+num-1)/num;
for(int i=1;i<=n;i++)f[i]=(i-1)/num+1;
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
val[f[i]].push_back(a[i]);
}
for(int i=1;i<=tot;i++)sort(val[i].begin(),val[i].end());
for(int id=1,l,r,v;id<=q;id++){
char opt[3];
scanf("%s",opt+1);
scanf("%lld%lld%lld",&l,&r,&v);
if(opt[1]=='M'){
for(int i=l;i<=min(r,f[l]*num);i++)
a[i]+=v;
reset(f[l]);
if(f[l]!=f[r]){
for(int i=(f[r]-1)*num+1;i<=r;i++)
a[i]+=v;
reset(f[r]);
}
for(int i=f[l]+1;i<=f[r]-1;i++)
block[i]+=v;
}
else{
int res=0;
for(int i=l;i<=min(r,f[l]*num);i++)if(a[i]+block[f[i]]>=v)++res;
if(f[l]!=f[r])for(int i=(f[r]-1)*num+1;i<=r;i++)if(a[i]+block[f[i]]>=v)++res;
for(int i=f[l]+1;i<=f[r]-1;i++){
int xx=v-block[i];
res+=val[i].size()-(lower_bound(val[i].begin(),val[i].end(),xx)-val[i].begin());
}
printf("%lld\n",res);
}
}
return 0;
}
一般题目中出现“输入是加密的”说明要求强制在线(不然怎么输入啊)。特地提出“l>r时交换”说明数据是随机的。这时候分块被卡的概率会很低。
这道题使用邪道做法:前缀和块元素个数,暴力扫块内元素,复杂度 \(O(n*num)\)。不过能过。
现在提供这种题的正经做法。
一般采用 \(ans_{i,j}\) 表示从第 \(i\) 个块到第 \(j\) 个块的答案和,每次查询暴力扫边角在将答案合并。
不过分块题的答案肯定不是可以简单合并的,在这道题中边角的数与块内的数一旦有重合就会影响总个数。\(ans_{i,j}\) 只能简单维护答案,如何判重?
考虑再使用 \(sum_{i,j}\) 表示块 \(i\) 开始元素 \(j\) 的前缀和。不过这种题一般要使用后缀和,会在之后解释。
对于一段区间 \([l,r]\),将其分为边角 \(a,b\) 与块区间 \([x,y]\) 后,对于元素 \(k\) 可以这样合并:
即原本算在 \([x,y]\) 答案中,但实际汇总个数不为偶数的数需删去。
即只在边角出现的偶数个元素需要加上
即边角与块内个数都为奇数的元素要加上,因为合起来就是偶数了。
并且处于复杂度我们不能枚举元素种类,注意到大部分块内的答案是不变的,只有边角 \(a,b\) 影响到的答案才需要进行上述的修改。
所以在扫边角时将可能对答案有影响的元素统计并入栈。处理块答案时再拿出来就行,\(|a|,|b|\le 2\sqrt n\),故计算也是 \(O(\sqrt n)\)。总复杂度 \(O(m\sqrt n)\)。
不过如何预处理?
显然预处理最多只能是 \(O(n\sqrt n)\) 的。我们按块下标作起点,变扫变统计当前时段有多少元素个数为偶数,到块边界的时候存下,注意到求和是顺带的,不过变成了后缀和。
但是我们发现总复杂度为 \(O((n+m)\sqrt n)\)。这显然偏离了分块的初衷即基本不等式优化暴力。令块长为 \(B\) 则有 \(n/B\) 个块,复杂度 \(O(n^2/B+mB)\)。
当且仅当 \(n^2/B=mB\Longrightarrow B=\sqrt{n^2/m}\) 时有最小。
不过这还不是最优解,因为在处理问题时复杂度会有常数,所以会设计出各种时空配置的分块。
这里只提供初步优化。
#include<bits/stdc++.h>
#define MAXN 100005
#define MAXM 1005
using namespace std;
int n,c,m;
int num,tot;
int f[MAXN];
int sum[MAXM][MAXN],block[MAXM][MAXM];
int a[MAXN],ans;
int val[MAXN],stac[MAXN],top;
int main(){
scanf("%d%d%d",&n,&c,&m);
while(num*num<n*n/m)++num;
tot=(n+num-1)/num;
for(int i=1;i<=n;i++)f[i]=(i-1)/num+1;
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=tot;i++){
int cnt=0;
for(int j=(i-1)*num+1;j<=n;j++){
++sum[i][a[j]];
if((sum[i][a[j]]%2==1)&&(sum[i][a[j]]>1))cnt--;
else if(sum[i][a[j]]%2==0)cnt++;
if(f[j]!=f[j+1])block[i][f[j]]=cnt;
}
}
for(int id=1,l,r;id<=m;id++){
scanf("%d%d",&l,&r);
l=(l+ans)%n+1,r=(r+ans)%n+1;
if(l>r)swap(l,r);
ans=0;
if(f[r]==f[l]){
for(int i=l;i<=r;i++){
++val[a[i]];
stac[++top]=a[i];
}
while(top){
if(val[stac[top]]){
if(val[stac[top]]%2==0)++ans;
val[stac[top]]=0;
}
--top;
}
printf("%d\n",ans);
continue;
}
if(f[l]+1<=f[r]-1)ans=block[f[l]+1][f[r]-1];
for(int i=l;i<=f[l]*num;i++){
++val[a[i]];
stac[++top]=a[i];
}
for(int i=(f[r]-1)*num+1;i<=r;i++){
++val[a[i]];
stac[++top]=a[i];
}
while(top){
int v=stac[top];
if(val[v]){
if((sum[f[l]+1][v]-sum[f[r]][v]>0)&&((sum[f[l]+1][v]-sum[f[r]][v])%2==0)&&(val[v]%2==1))--ans;
if(((sum[f[l]+1][v]-sum[f[r]][v])==0)&&((val[v]%2)==0))++ans;
if((sum[f[l]+1][v]-sum[f[r]][v]>0)&&((sum[f[l]+1][v]-sum[f[r]][v])%2==1)&&(val[v]%2==1))++ans;
val[v]=0;
}
--top;
}
printf("%d\n",ans);
}
return 0;
}
对于蒲公英那道题,做法是相似的,维护区间众数与数字前后缀和,显然只有边角数字会影响答案,扫一遍边角数字重新统计即可。
求 \([l,r]\) 内颜色 \([a,b]\) 数的平方之和,(最开始想分二维的块),区间颜色权值之和必须要先算单色个数,再平方,再相加,这样的复杂度是很高的。
考虑 \(sum_{i,j,k}\) 表示第 \(i\) 到 \(j\) 块内颜色 \(k\) 的出现次数,\(block_{i,j,k}\) 表示对这个数字平方并前缀和。
统计元素个数时,碎块元素数 \(val_1\) 与整块元素数 \(val_2\) 对整块平方数 \(v\) 的贡献:
为了少枚举一次颜色,单个增加时有:
设块长为 \(B\) 我们发现:好像预处理就会爆啊!枚举块+颜色汇总少说是 \(O(m(\frac{n}{B})^2)\) 的。
不过还是可以不等式的。
为了方便处理令 \(n=m=q\),此时块数不超过 50,即可预处理。
#include<bits/stdc++.h>
#define MAXN 20005
#define MAXM 50
#define N 50005
using namespace std;
int n,m,q;
int num,tot;
int a[N],f[N];
int val[MAXN];
int sum[MAXM][MAXM][MAXN],block[MAXM][MAXM][MAXN];
int ans,top,stac[N];
int main(){
scanf("%d%d%d",&n,&m,&q);
while((long long)num*num*num<=(long long)n*n)++num;
tot=(n+num-1)/num;
for(int i=1;i<=n;i++)f[i]=(i-1)/num+1;
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=tot;i++){
for(int j=i;j<=tot;j++){
for(int k=(j-1)*num+1;k<=min(n,j*num);k++)++val[a[k]];
for(int k=1;k<=m;k++)sum[i][j][k]=val[k],block[i][j][k]=val[k]*val[k]+block[i][j][k-1];
}
for(int j=1;j<=m;j++)val[j]=0;
}
for(int id=1,l,r,A,B;id<=q;id++){
scanf("%d%d%d%d",&l,&r,&A,&B);
if(ans)l^=ans,r^=ans,A^=ans,B^=ans;
ans=0;
if(l>r)swap(l,r);
if(A>B)swap(A,B);
if(f[r]==f[l]){
int res=0;
for(int i=l;i<=r;i++){
if(a[i]>=A&&a[i]<=B)res+=val[a[i]]*2+1,++val[a[i]],stac[++top]=a[i];
}
while(top){
if(val[stac[top]])val[stac[top]]=0;
--top;
}
printf("%d\n",res);
ans=res;
continue;
}
ans=block[f[l]+1][f[r]-1][B]-block[f[l]+1][f[r]-1][A-1];
for(int i=l;i<=min(r,f[l]*num);i++){
if(a[i]>=A&&a[i]<=B)
ans+=(sum[f[l]+1][f[r]-1][a[i]]+val[a[i]])*2+1,++val[a[i]],stac[++top]=a[i];
}
for(int i=(f[r]-1)*num+1;i<=r;i++){
if(a[i]>=A&&a[i]<=B)
ans+=(sum[f[l]+1][f[r]-1][a[i]]+val[a[i]])*2+1,++val[a[i]],stac[++top]=a[i];
}
while(top){
if(val[stac[top]])val[stac[top]]=0;
--top;
}
printf("%d\n",ans);
}
return 0;
}
莫队
给定一段序列疯狂求区间和。
幼年不会线段树时,我想到不同区间的答案是可以线性转移的,且如果让区间尽可能的近,那么转移就尽可能的快,当时就想到将查询序列离线排序处理答案。
这就是莫队的主要思路,非常简单,不过我当时不会分块划分,所以复杂度是假的。
事实上更快的莫队需要更小的总跳跃次数,严格上讲要对查询序列跑曼哈顿距离最小生成树。不过可以用分块搞到 \(O(n\sqrt n)\)。即对查询序列左端点按块递增排序,右端点做第二关键字递增。
证明:排序后左端点按块递增,左端点块相同时右端点递增,故单个块的右端点最多跳 \(O(n)\) 次,块内左端点跳左端点最多跳 \(O(B)\)。
总复杂度\(O(\frac{n^2}{B}+qB)\),取 \(n=q\),则 \(B=\sqrt n\) 时有最小复杂度 \(O(n\sqrt n)\)。
这也揭示了莫队的使用范畴
- 必须可以离线。
- 必须可以快速(\(O(1),O(logn)\))跳下标转移答案。
- 不得有复杂修改。
这个题如果用分块会导致内存爆炸。不过没要求在线,考虑莫队。
设当前元素 \(k\) 有 \(val_k\) 个,则新增一个 \(k\) 时对答案的贡献:
减少一个时的贡献:
然后就没了。
#include<bits/stdc++.h>
#define MAXN 50005
#define int long long
using namespace std;
int n,m,k;
int a[MAXN],f[MAXN];
int num,tot;
struct node{
int l,r,id;
}q[MAXN];
inline bool cmp(node x,node y){
if(f[x.l]==f[y.l])return x.r<y.r;
return f[x.l]<f[y.l];
}
int ll=1,rr,res;
int ans[MAXN],val[MAXN];
inline void Add(int x){
res+=val[a[x]]*2+1;
++val[a[x]];
}
inline void Del(int x){
res-=(val[a[x]]-1)*2+1;
--val[a[x]];
}
signed main(){
scanf("%lld%lld%lld",&n,&m,&k);
num=sqrt(n);
tot=(n+num-1)/num;
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
for(int i=1;i<=n;i++)f[i]=(i-1)/num+1;
for(int i=1;i<=m;i++){
scanf("%lld%lld",&q[i].l,&q[i].r);
q[i].id=i;
}
sort(q+1,q+1+m,cmp);
for(int i=1,l,r;i<=m;i++){
l=q[i].l,r=q[i].r;
while(l<ll)--ll,Add(ll);
while(l>ll)Del(ll),++ll;
while(r<rr)Del(rr),--rr;
while(r>rr)++rr,Add(rr);
ans[q[i].id]=res;
}
for(int i=1;i<=m;i++)printf("%lld\n",ans[i]);
return 0;
}
在长为 \(siz\) 的区间 \([l,r]\) 中有 \(\frac{siz(siz-1)}{2}\) 种双选取法,假设其中的 \(k\) 种元素各有 \(val_i\) 个,则概率:
增减一个元素 \(k\) 对分子的影响为 \(val_k\) 或 \(val_k-1\)。
然后就没了。
莫队的扩展性比较强。
本题中,开两棵 BIT 维护值域 \([l,r]\) 间的元素种数与元素个数。
时间复杂度 \(O(n\sqrt n\ logn)\)
不过在根号算法中再使用 \(log n\) 复杂度的东西会大幅增加时间复杂度,稍微卡一卡就 T 了。
考虑优化,我们发现如果在维护值域的过程中引入分块,即将值域分块,维护块内外的答案。那么单次块上的查询是 \(\sqrt n\) 的。
不过只需在修改后查询,也就是说莫队操作的复杂度与查询的复杂度是独立的,总复杂度 \(O(n\sqrt n)\)。
不考虑修改的话这道题算莫队板题。不过现在有修改了。
这个东西叫做带修莫队,即支持简单的修改操作。
莫队对每个询问区间 \([l,r]\) 组成的二元组建系,沿轴进行 \(O(1)\) 的添加删除操作。现在考虑时间维度,一次位于 \(t\) 的修改只会对 \(t_x>t\) 的查询操作造成影响。
具体地,在这道题中,在刚才的基础上只有位于查询区间的修改才能对该区间的答案造成影响。
所以参考 \(l X r\) 二维平面,排序后让莫队在三维上移动。
#include<bits/stdc++.h>
#define MAXN 1500000
#define int long long
using namespace std;
int n,m;
int num,tot;
int f[MAXN],a[MAXN];
struct node{
int l,r,t,id;
}qa[MAXN],qm[MAXN];
int va,vm;
inline bool cmp(node x,node y){
return f[x.l]==f[y.l]?(f[x.r]==f[y.r]?x.t<y.t:x.r<y.r):(x.l<y.l);
}
int ll=1,rr,tt,res;
int val[MAXN];
inline void Add(int x){
if(!val[a[x]])++res;
++val[a[x]];
}
inline void Del(int x){
--val[a[x]];
if(!val[a[x]])--res;
}
inline void Upd(int x,int tim){
if(qa[x].l<=qm[tim].l&&qm[tim].l<=qa[x].r){
Del(qm[tim].l);
int v=qm[tim].r;
if(!val[v])++res;
++val[v];
}
swap(a[qm[tim].l],qm[tim].r);
}
int ans[MAXN];
signed main(){
scanf("%lld%lld",&n,&m);
while(n*n>num*num*num)++num;
tot=(n+num-1)/num;
for(int i=1;i<=n;i++)f[i]=(i-1)/num+1;
for(int i=1;i<=n;i++)scanf("%lld",&a[i]);
for(int i=1,l,r;i<=m;i++){
char opt[5];
scanf("%s%lld%lld",opt+1,&l,&r);
if(opt[1]=='Q'){
++va;
qa[va].l=l,qa[va].r=r,qa[va].t=vm;
qa[va].id=va;
}
else{
qm[++vm].l=l,qm[vm].r=r;
}
}
sort(qa+1,qa+1+va,cmp);
for(int i=1,l,r,t;i<=va;i++){
l=qa[i].l,r=qa[i].r,t=qa[i].t;
while(l>ll)Del(ll++);
while(l<ll)Add(--ll);
while(r>rr)Add(++rr);
while(r<rr)Del(rr--);
while(t<tt)Upd(i,tt--);
while(t>tt)Upd(i,++tt);
ans[qa[i].id]=res;
}
for(int i=1;i<=va;i++)printf("%lld\n",ans[i]);
return 0;
}
更高级的技巧会在足够强后续写。