分块和莫队
分块
1.概述
分块可以用于处理对于一段区间的修改和查询操作,当然线段树等多种数据结构也可以实现这一功能。相较而言,分块码量较短,比较好想,但细节也并不少。
分块的核心操作是将区间划分为长度相等的若干块,对于每块长度,一般取\(sqrt(n)\),但有时要具体问题具体分析。当遇到一段需要查询或修改的区间\([l,r]\),对于它覆盖的整块,以块为单位进行处理,不完整的块暴力做,均摊下来复杂度是可以接受的。
2.模版
1.块的划分
int s=sqrt(n);//块长
for(int i=1;i<=n;i++){
if(i%s==1) bl[i]=bl[i-1]+1;
else bl[i]=bl[i-1];
}
2.修改&查询
以区间修改,单点查询为例
for(int i=1;i<=n;i++){
int opt,l,r,c;
scanf("%lld%lld%lld%lld",&opt,&l,&r,&c);
if(opt==0){
if(bl[l]==bl[r]) for(int j=l;j<=r;j++) a[j]+=c;
else{
for(int j=l;bl[j]==bl[l];j++) a[j]+=c;
for(int j=r;bl[j]==bl[r];j--) a[j]+=c;
for(int j=bl[l]+1;j<bl[r];j++) tag[j]+=c;
}
}
else printf("%lld\n",a[r]+tag[bl[r]]);
}
3.一些例题
分块结合二分,vector等可以实现多种信息的处理,这些信息可能无法用线段树或树状数组来做。具体的请结合上面的例题来看,这里不再赘述。但分块的时间复杂度一般劣于线段树,当二者都可以拿来做时,需要合理选择。
特别的,对于块的长度的选择,不一定都是\(sqrt(n)\),这样可能会导致时间复杂度退化。因此要用均值不等式来算。
4.易错点
1.别忘了给S赋值
2.注意懒标记的标记和撤销。大块直接打标记即可,对于小块,很多时候要先把之前的块标记下放给这个块内每个点,再对要修改的小区间暴力修改,最后还要清空标记。
询问时答案即为\(a[i]+tag[bl[i]]\)
3.\(i\),\(j\)不要写反
4.给\(bl[i]\)赋值时,不要忘记写\(else\)...
5.临时桶数组记得清空。有时小块暴力用的桶数组和整块用的桶数组要区分开
莫队
1.概述
莫队算法借助了分块的思想,一般用于不带修改的区间询问问题(当然也有带修改莫队)。莫队算法的关键是离线查询。莫队算法可以解决一类离线区间询问问题,适用性极为广泛。同时将其加以扩展,便能轻松处理树上路
径询问以及支持修改操作。
假设有一个离线询问\([l,r]\),我们已经通过某种方法得到了这个询问的答案(例如暴力),那么利用转移的思想,我们尝试把询问\([l,r]\)的答案推广到\([l-1,r]\),\([l+1,r]\),\([l,r-1]\) ,\([l,r+1]\) 这四个问题
的答案,当然,一般来说我们要求这个转移的耗时是极少的(例如\(O(1)\))。如果这种推广是可行的,那么就能用这种方法把一个已知的区间答案不断推广到另一个我们需要的区间答案,这就是莫队算法。
2.模版
模版题见HH的项链
核心代码如下
int ql=1,qr=0;
for(int i=1;i<=m;i++){
int l=o[i].l,r=o[i].r;
while(ql>l) add(a[--ql]);
while(ql<l) del(a[ql++]);
while(qr<r) add(a[++qr]);
while(qr>r) del(a[qr--]);
ans[o[i].id]=sum;
}
两个函数
void add(int x){
cnt[x]++;
if(cnt[x]==1) sum++;
return;
}
void del(int x){
cnt[x]--;
if(cnt[x]==0) sum--;
return;
}
别忘了先分块
3.回滚莫队
有一类问题,你发现显然用莫队来做。当你码到一半时,突然发现删点或加点操作难以维护。这时就要用回滚莫队。具体来说,回滚莫队一般有只加不删和只删不加两种。这里以只加不删为例。
当加点操作容易实现,删点操作无法做时,采用以下流程的回滚莫队:
(1.) 对原序列进行分块,并对询问按照如下的方式排序:以左端点所在的块升序为第一关键字,以右端点升序为第二关键字
(2.) 对于左右端点在同一个块中的询问,我们直接暴力扫描回答即可,时间复杂度为\(O(sqrt(n))\)。
(3.) 对于处理所有左端点在块\(T\)内的询问,我们先将莫队区间左端点初始化为\(R[T]+1\),右端点初始化为\(R[T]\) ,这是一个空区间
(4.) 对于左右端点不在同一个块中的所有询问,由于其右端点升序,我们对右端点只做加点操作,总共最多加点\(n\)次
(5.) 对于左右端点不在同一个块中的所有询问,其左端点是可能乱序的,我们每一次从\(R[T]+1\)的位置出发,只做加点操作,到达询问位置即可,每一个询问最多加\(sqrt(n)\)次。回答完询问后,我们撤销本次移动左端点的所有改动,使左端点回到\(R[T]+1\)的位置
(6.) 按照相同的方式处理下一块
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e6+10;
int n,q;
ll sum=0,mx=0,a[maxn];
int c[maxn],bl[maxn],cnt[maxn],cnt1[maxn],L[maxn],R[maxn];
vector<int>b;
struct node{
int l,r,id;
bool friend operator <(node a,node b){
return bl[a.l]==bl[b.l]?a.r<b.r:a.l<b.l;
}
}o[maxn];
ll ans[maxn];
void add(int x,ll &s){//修改
cnt[a[x]]++;
s=max(s,1ll*cnt[a[x]]*b[a[x]-1]);
}
void del(int x){
cnt[a[x]]--;
}
int main(){
cin>>n>>q;
int s=sqrt(n);
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++){
if(i%s==1){
bl[i]=bl[i-1]+1;
L[bl[i]]=i;
}
else bl[i]=bl[i-1];
b.push_back(a[i]);
R[bl[i]]=i;
}
sort(b.begin(),b.end());//普通离散化
b.erase(unique(b.begin(),b.end()),b.end());
for(int i=1;i<=n;i++){
a[i]=lower_bound(b.begin(),b.end(),a[i])-b.begin()+1;
}
for(int i=1;i<=q;i++){
cin>>o[i].l>>o[i].r;
o[i].id=i;
}
sort(o+1,o+q+1);
int ql=1,qr=0,las=0;
for(int i=1;i<=q;i++){
int l=o[i].l,r=o[i].r;
if(bl[l]==bl[r]){//l,r相同的直接暴力
ll now=0;
for(int j=l;j<=r;j++) cnt1[a[j]]++;
for(int j=l;j<=r;j++) now=max(now,1ll*b[a[j]-1]*cnt1[a[j]]);
for(int j=l;j<=r;j++) cnt1[a[j]]=0;
ans[o[i].id]=now;
continue;
}
if(las^bl[l]){//换块时重置左右端点
while (ql<R[bl[l]]+1) del(ql++);
while (qr>R[bl[l]]) del(qr--);
mx=0;
las=bl[l];
}
while(qr<r) add(++qr,mx);//右端点右移并加点
ll now=mx;//这里很重要,左端点在临时移动后还要返回,因此在左边统计的答案用now来表示,让now先继承mx
while(ql>l) add(--ql,now);//左端点从初始位置向左到达询问位置
while(ql<R[bl[l]]+1) del(ql++);//回滚并撤销操作
ans[o[i].id]=now;//记录答案
}
for(int i=1;i<=q;i++) cout<<ans[i]<<endl;
return 0;
}
要注意的是,当你分析到加点或删点操作难以实现时,才考虑用回滚莫队。
4.带修莫队
前面提到过,莫队只能用于处理不带修改的询问。事实上,存在一种带修莫队,在普通莫队的基础上又加了一维,称为时间维。
具体来讲,我们的做法是把修改操作编号,称为"时间戳",而查询操作的时间戳沿用之前最近的修改操作的时间戳。
跑主算法时定义当前时间戳为\(t\),对于每个查询操作,如果当前时间戳相对小了,说明已进行的修改操作比要求的多,就把之前改的改回来,反之往后改。只有当 当前区间和查询区间左右端点、时间戳均重合时,才认定区间完全重合,此时的答案才是本次查询的最终答案。
带修莫队的排序同样加上了一个时间:
struct node1{
int l,r,tim,id;
bool friend operator <(node1 a,node1 b){
if(bl[a.l]!=bl[b.l]) return a.l<b.l;
if(bl[a.r]!=bl[b.r]) return a.r<b.r;
return a.tim<b.tim;
}
}ques[maxn];
需要特别注意的是,带修莫队的块长应取\(n^\frac{2}{3}\),这样总的复杂度为\(O(n^\frac{5}{3})\),证明略
具体实现如下
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+10;
int n,m,bl[maxn],cnt[maxn],col[maxn];
int sum=0,ans[maxn];
struct node1{
int l,r,tim,id;
bool friend operator <(node1 a,node1 b){
if(bl[a.l]!=bl[b.l]) return a.l<b.l;
if(bl[a.r]!=bl[b.r]) return a.r<b.r;
return a.tim<b.tim;
}
}ques[maxn];
struct node2{
int pos,c;
}chg[maxn];
int S,qt=0,ct=0;
void add(int x){//加点
cnt[x]++;
if(cnt[x]==1) sum++;
return;
}
void del(int x){//删点
cnt[x]--;
if(!cnt[x]) sum--;
return;
}
void addt(int x,int l,int r){//处理修改操作
if(chg[x].pos>=l&&chg[x].pos<=r){
cnt[chg[x].c]++;
cnt[col[chg[x].pos]]--;
if(cnt[chg[x].c]==1) sum++;
if(!cnt[col[chg[x].pos]]) sum--;
}
swap(col[chg[x].pos],chg[x].c);//修改。每到这一层,就改一次,这样总能得到正确的状态
return;
}
int main(){
cin>>n>>m;
S=pow(n,2.0/3.0);//注意块长
for(int i=1;i<=n;i++) cin>>col[i];
for(int i=1;i<=m;i++){
char C;
int l,r;
cin>>C>>l>>r;
if(C=='Q') ques[++qt]=node1{l,r,ct,qt};
else chg[++ct]=node2{l,r};
}
for(int i=1;i<=n;i++){
if(i%S==1) bl[i]=bl[i-1]+1;
else bl[i]=bl[i-1];
}
sort(ques+1,ques+qt+1);
int ql=1,qr=0,t=0;
for(int i=1;i<=qt;i++){
int l=ques[i].l,r=ques[i].r;
while(ql>l) add(col[--ql]);//普通莫队找区间
while(ql<l) del(col[ql++]);
while(qr>r) del(col[qr--]);
while(qr<r) add(col[++qr]);
while(t<ques[i].tim) addt(++t,l,r);//将时间移动到离询问最近的前一次修改
while(t>ques[i].tim) addt(t--,l,r);
ans[ques[i].id]=sum;
}
for(int i=1;i<=qt;i++) cout<<ans[i]<<endl;
return 0;
}
5.树上莫队
望文生义,自然是对树进行操作的一种莫队。树上莫队实质上是将树转化为一个序列,在这个序列上进行操作。这个序列就是括号序。具体我们结合例题来看。
例题:SP10707 COT2 - Count on a tree II
如果不是在树上,那么就是普通莫队板子。在树上怎么做?我们考虑一个序列\(S\),对树进行一次\(dfs\)遍历,遍历到哪个节点,就把那个节点放进\(S\)里,等遍历完这个节点的子树,将要离开这个节点时,再次把这个节点放进\(S\)里。这样序列\(S\)就称为是这棵树的括号序。显然一个节点\(x\)在\(S\)中会出现两次,那么我们记节点\(x\)在\(S\)中出现的第一个位置为\(l[x]\),出现的第二个位置为\(r[x]\)。
1.对于两个不同节点\(u\),\(v\),如果\(l[u]<l[v]<r[v]<r[u]\),那么\(v\)一定是\(u\)的子树上的节点。
2.对于两个不同节点\(u\),\(v\),如果\(lca(u,v)=v\)或\(lca(u,v)=v\),那么从\(u\)到\(v\)的路径上,经过的节点即为\(S\)中\(l[u]\)到\(l[v]\)之间只出现一次的点;如果上面的条件不满足,那么从\(u\)到\(v\)的路径上,经过的节点包括\(S\)中\(r[u]\)到\(l[v]\)之间只出现一次的点,再加上\(lca(u,v)\)。
上面两个条件,画图模拟一下即可得到。拿着1和2,我们就可以把树上莫队转化为普通莫队来做了。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=2e5+10;
int n,m,s[maxn],v[maxn];
int id=0,L[maxn],R[maxn];
vector<int>l[maxn];
int f[maxn][40],d[maxn];
int bl[maxn],cnt[maxn],used[maxn],sum=0,ans[maxn];
struct node{
int l,r,id,Lca;
bool friend operator <(node a,node b){
return bl[a.l]==bl[b.l]?a.r<b.r:a.l<b.l;
}
}o[maxn];
void add(int x){
cnt[v[x]]++;
if(cnt[v[x]]==1) sum++;
return;
}
void del(int x){
cnt[v[x]]--;
if(!cnt[v[x]]) sum--;
return;
}
void chg(int x){
used[s[x]]^=1;
if(used[s[x]]) add(s[x]);
else del(s[x]);
}
void dfs(int p,int fa){
d[p]=d[fa]+1;
f[p][0]=fa;
s[++id]=p;
L[p]=id;
for(int i=1;i<=31;i++) f[p][i]=f[f[p][i-1]][i-1];
for(int i=0;i<l[p].size();i++){
int y=l[p][i];
if(y==fa) continue;
dfs(y,p);
}
s[++id]=p;
R[p]=id;
}
int lca(int x,int y){
if(d[x]>d[y]) swap(x,y);
int siz=d[y]-d[x];
for(int i=0;siz;i++,siz>>=1) if(siz&1) y=f[y][i];
if(x==y) return x;
for(int i=31;i>=0&&x!=y;i--){
if(f[x][i]!=f[y][i]){
x=f[x][i];
y=f[y][i];
}
}
return f[x][0];
}
signed main(){
cin>>n>>m;
int S=sqrt(n);
for(int i=1;i<=n;i++) cin>>v[i];
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
l[x].push_back(y);
l[y].push_back(x);
}
dfs(1,0);
for(int i=1;i<=id;i++){
if(i%S==1) bl[i]=bl[i-1]+1;
else bl[i]=bl[i-1];
}
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
if(L[x]>L[y]) swap(x,y);
int Lca=lca(x,y);
if(Lca==x||Lca==y){
o[i].l=L[x];
o[i].r=L[y];
}else{
o[i].l=R[x];
o[i].r=L[y];
o[i].Lca=Lca;
}
o[i].id=i;
}
sort(o+1,o+m+1);
int ql=1,qr=0;
for(int i=1;i<=m;i++){
int l=o[i].l,r=o[i].r;
while(ql>l) chg(--ql);
while(ql<l) chg(ql++);
while(qr<r) chg(++qr);
while(qr>r) chg(qr--);
ans[o[i].id]=sum;
if(o[i].Lca&&cnt[v[o[i].Lca]]==0) ans[o[i].id]++;
}
for(int i=1;i<=m;i++) cout<<ans[i]<<endl;
return 0;
}
5.一些注意事项
1.莫队以分块和排序为基础,因此当\(TLE\)时优先检查分块和排序。
2.一般来说普通排序就可以过,必要时采用奇偶性优化,但是回滚莫队决不能用奇偶性优化,不然大寄。另外,带修莫队在排序时还要加上时间戳。
3.树上莫队是在树的括号序上进行操作,树上莫队是在树的括号序上进行操作,树上莫队是在树的括号序上进行操作。
4.写莫队尤其要注意顺序问题,这包括左右端点移动时的顺序,删点加点时更新的顺序等等。
5.当你遇到树上带修莫队,请参照这个板子,题目为P4074糖果公园
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=2e5+10;
vector<int>l[maxn];
int n,m,q,v[maxn],w[maxn],S,bl[maxn],c[maxn];
int s[maxn],f[maxn][40],dep[maxn],L[maxn],R[maxn];
int sc=0,qc=0,cnt[maxn],cntt=0,sum=0,ans[maxn];
struct node1{
int l,r,id,tim;
int Lca;
bool friend operator <(node1 a,node1 b){
if(bl[a.l]!=bl[b.l]) return a.l<b.l;
if(bl[a.r]!=bl[b.r]) return a.r<b.r;
return a.tim<b.tim;
}
}ques[maxn];
struct node2{
int pos,col;
}chg[maxn];
void dfs(int p,int fa){
s[++cntt]=p;
L[p]=cntt;
dep[p]=dep[fa]+1;
f[p][0]=fa;
for(int i=1;i<=31;i++) f[p][i]=f[f[p][i-1]][i-1];
for(int i=0;i<l[p].size();i++){
int y=l[p][i];
if(y==fa) continue;
dfs(y,p);
}
s[++cntt]=p;
R[p]=cntt;
}
int lca(int x,int y){
if(dep[x]>dep[y]) swap(x,y);
int siz=dep[y]-dep[x];
for(int i=0;siz;i++,siz>>=1) if(siz&1) y=f[y][i];
if(x==y) return x;
for(int i=31;i>=0&&x!=y;i--){
if(f[x][i]!=f[y][i]){
x=f[x][i];
y=f[y][i];
}
}
return f[x][0];
}
void add(int x){
cnt[x]++;
sum+=v[x]*w[cnt[x]];
return;
}
void del(int x){
sum-=v[x]*w[cnt[x]];
cnt[x]--;
}
int QY[maxn];//%%%
void change(int x){
if(!QY[x]){
add(c[x]);
}
else{
del(c[x]);
}
QY[x]^=1;
}
void chgt(int x,int l,int r){
if(QY[chg[x].pos]){
del(c[chg[x].pos]);
add(chg[x].col);
}
swap(c[chg[x].pos],chg[x].col);
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m>>q;
for(int i=1;i<=m;i++) cin>>v[i];
for(int i=1;i<=n;i++) cin>>w[i];
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
l[x].push_back(y);
l[y].push_back(x);
}
dfs(1,0);
for(int i=1;i<=n;i++) cin>>c[i];
for(int i=1;i<=q;i++){
int tp,x,y;
cin>>tp>>x>>y;
if(!tp) chg[++sc]={x,y};
else{
ques[++qc]={x,y,qc,sc};
if(L[x]>L[y]) swap(x,y);
int Lca=lca(x,y);
if(Lca==x){
ques[qc].l=L[x];
ques[qc].r=L[y];
ques[qc].Lca=0;
}else{
ques[qc].l=R[x];
ques[qc].r=L[y];
ques[qc].Lca=Lca;
}
}
}
n*=2;
S=pow(n,2.0/3.0);
int nnum=0;
for(int i=1;i<=n;i++){
if(i%S==1) nnum++;
bl[i]=nnum;
}
sort(ques+1,ques+1+qc);
int ql=1,qr=0,t=0;
for(int i=1;i<=qc;i++){
int l=ques[i].l,r=ques[i].r;
while(ql>l) change(s[--ql]);
while(ql<l) change(s[ql++]);
while(qr>r) change(s[qr--]);
while(qr<r) change(s[++qr]);
while(t<ques[i].tim) chgt(++t,l,r);
while(t>ques[i].tim) chgt(t--,l,r);
if(ques[i].Lca) change(ques[i].Lca);
ans[ques[i].id]=sum;
if(ques[i].Lca) change(ques[i].Lca);
}
for(int i=1;i<=qc;i++) cout<<ans[i]<<endl;
return 0;
}
块长仍沿用带修莫队。
$(Maybe)\ To \ be \ continued \ $...- [ ]

浙公网安备 33010602011771号