莫队

一年前我初见莫队,一年后我才正式学习莫队,现在kang来一年之际沧海桑田……

套用某叶老师的话来说,“缘分这个东西真的很奇妙”。

好吧。我开始初学莫队啦。

概述

莫队是一种离线算法。它主要针对那些区间问题,而且需要维护的信息比较复杂而无法使用线段树等传统数据结构维护的问题。它需要满足两个性质,一个是题目支持离线(什么强制在线异或加密的莫队搞不定),二是区间答案具有较强的“可加性”与可减性,也就是说,知道了[l,r]的答案,可以很快地求出[l,r+1]或[l+1,r] 的答案。

个人觉得,莫队的本质是利用已求出的的答案来优化后续求解的过程。

一般来说,莫队的复杂度为 \(O(N\sqrt{N})\) 或者再加一个log(如果带修的话可能是\(N^{\frac{5}{3}}\)),一般来说可以应对\(10^5\)级别的数据量。实际过程中会发现写出来还是挺简单的,而且具有极强的拓展性。

接下来对四种莫队进行概述。

普通莫队

它能完成的就是多次询问区间的一些信息,比如区间内有多少个元素出现过奇数次。很显然这个问题无法使用线段树来维护(主席树似乎也不行),直接用分块的话复杂度过高,于是想到使用莫队(事实上这是最基础的那一类莫队)。

算法过程一般是这样的:

首先对询问进行分块排序,保证复杂度(当然要记录询问出现的位置方便输出):

inline bool cmp(node s1,node s2){
	if(s1.l/bl==s2.l/bl)return s1.r<s2.r;
	else return s1.l<s2.l;
}//bl一般取根号N

然后就是莫队主体

inline void add(int wh){
	//加入操作并且更新答案
}
inline void del(int wh){
	//同上,处理删除操作
}
signed main(){
	......
	int l=0,r=0;//初始化
	for(int i=1;i<=m;i++){//按顺序处理每一个询问
		while(r<q[i].r)add(a[++r]);
		while(l>q[i].l)add(a[--l]);
		while(r>q[i].r)del(a[r--]);
		while(l<q[i].l)del(a[l++]);
		//将莫队区间移到当前的询问区间。注意自加自减的写法
		ans[q[i].id]=ans;
	}
	//输出即可
	......
}

带修莫队

其实吧,带修莫队可以看成是一种三维序列(三维的能叫序列?大雾)的问题。唯一不同的是它应该先挪时间轴,而且只有当修改点在当前的莫队区间里(是莫队区间而不是询问区间!血与泪的教训!)才对答案进行更新。另外就是为了平衡复杂度,块的大小一般取\(N^{\frac{5}{3}}\),证明方法懒得写了,反正这种结论性的东西会用就行。

用P1903的代码做一下示范:

//排序的cmp:
inline bool cmp(node s1,node s2){
	if(s1.l/bl!=s2.l/bl)return s1.l/bl<s2.l/bl;
	else if(s1.r/bl!=s2.r/bl)return s1.r/bl<s2.r/bl;
	else return s1.t<s2.t;
}
//其中bl=ceil(pow(N,0.66))+1;

//挪指针:
for(int i=1;i<=cnt;i++){
	//重点是挪时间轴中的判断
	//当时由于人傻了调了一个半小时才调出来
	while(t<q[i].t){
		t++;a[ch[t].pl]=ch[t].data;
		if(ch[t].pl>=l&&ch[t].pl<=r){
			del(ch[t].bf);
			add(ch[t].data);
		}
	}
	while(t>q[i].t){
		if(ch[t].pl>=l&&ch[t].pl<=r){
			del(ch[t].data);
			add(ch[t].bf);
		}
		a[ch[t].pl]=ch[t].bf;t--;
	}
	while(r<q[i].r)add(a[++r]);
	while(l>q[i].l)add(a[--l]);
	while(r>q[i].r)del(a[r--]);
	while(l<q[i].l)del(a[l++]);
	an[q[i].id]=ans;
}

回滚莫队

名字很高大上,然而我并不觉得它那个讲法很通俗易懂。

回滚莫队针对那种添加元素和删除元素其中一个统计答案比较复杂或复杂度比较高的问题,也就是说它只支持把区间越扩越大而不支持把区间弄得稍微小一点。这就与我们的初衷产生了分歧,也就是说相交却不包含的两个区间无法互相得到答案,那么复杂度也就无从谈起了。

但是天无绝人之路。我们可以考虑对于这些询问区间把它们砍成两半,那么我们可以强迫其中的一些半区间互相包含(简单的一种做法是按照同样的分界线去砍,这样就会出现许多左端点对齐、右端点不同,当然可以让它们递增的半区间,这样旧就使得这些询问在某种意义上变得互相包含了),然后让剩下的部分暴力求解即可。

实现上,可以考虑根号分块,把所有跨块区间按照左端点所在块进行分类,然后分批进行处理。处理方法如上文,对于右端点排序从小到大依次处理,左半段暴力即可。至于那些块内区间,很明显它们的长度不会太大,暴力求解即可。

总的复杂度为\(O(N\sqrt{N})\),常数可能比普通莫队大一些。

由于实现过程中要多次清空数据结构,可以考虑进行节点的时间标记优化。

比如这道题就要用回滚莫队(我真的没压行!):历史研究

#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
//#define zczc
#define ll long long
using namespace std;
const int N=100010;
inline void read(int &wh){
    wh=0;int f=1;char w=getchar();
    while(w<'0'||w>'9'){if(w=='-')f=-1;w=getchar();}
    while(w<='9'&&w>='0'){wh=wh*10+w-'0';w=getchar();}
    wh*=f;return;
}
int m,n,num,bl,a[N],b[N],s[N],t[N],nt;
ll an[N],ans,rans;
struct node{int l,r,pl;}q[N];
inline bool cmp(node s1,node s2){
	int r1=(s1.l-1)/bl,r2=(s2.l-1)/bl;
	if(r1==r2)return s1.r<s2.r;
	else return r1<r2;
}
inline void check(ll &s1,ll s2){if(s1<s2)s1=s2;return;}
inline void add(int wh){
	if(t[wh]!=nt)t[wh]=nt,s[wh]=0;s[wh]++;
	check(ans,(ll)b[wh]*s[wh]);
}
inline void del(int wh){s[wh]--;return;}
int ti[N],numm[N],nti;
inline void radd(int wh){
	if(ti[wh]!=nti){ti[wh]=nti;numm[wh]=0;}
	numm[wh]++;
	check(rans,(ll)b[wh]*numm[wh]);
}
void solve(int wh){
	nti++;rans=0;
	for(int i=q[wh].l;i<=q[wh].r;i++){radd(a[i]);}
	an[q[wh].pl]=rans;
}
signed main(){
	#ifdef zczc
	freopen("in.txt","r",stdin);
	#endif
	read(m);read(n);
	for(int i=1;i<=m;i++){read(a[i]);b[i]=a[i];}
	sort(b+1,b+m+1);num=unique(b+1,b+m+1)-b-1;
	for(int i=1;i<=m;i++)a[i]=upper_bound(b+1,b+num+1,a[i])-b-1;
	for(int i=1;i<=n;i++){read(q[i].l);read(q[i].r);q[i].pl=i;}
	bl=(int)ceil(sqrt(m));sort(q+1,q+n+1,cmp);
	int now=1,lim=bl,l,r;ll rec;
	while(now<=n){
		ans=0;nt++;l=lim+1,r=lim;
		while(q[now].l<=lim&&q[now].l>lim-bl&&now<=n){
			if(q[now].r<=lim){
				solve(now);
				now++;
				continue;
			}
			while(r<q[now].r)add(a[++r]);
			rec=ans;l=lim+1;
			while(l>q[now].l)add(a[--l]);
			an[q[now].pl]=ans;
			while(l<=lim)del(a[l++]);
			ans=rec;now++;
		}
		lim+=bl;
	}
	for(int i=1;i<=n;i++)printf("%lld\n",an[i]);
	return 0;
}

树上莫队

莫队完成了从无脊椎动物到有脊椎动物的蜕变!

它直立行走了!

——它可以处理树上的问题啦!

当然它只能处理路径上的问题,不过这已经很不错了。同样,它维护的是线段树(也就是树剖的底层数据结构)难以维护的信息,比如颜色种类……实现上还是把树上问题转换成序列问题,这里要借助一个东西就是欧拉序(tmd怎么到处都有欧拉!)

欧拉序就是升级版的dfn序,据说还可以用来\(O(1)\)在线求lca(虽然要\(O(N\log N)\)的预处理就显得非常之拉跨),当然也可以用来处理树上莫队。具体做法懒得写了,网上写得一个比一个好。

比如这道题:苹果树,约等于是询问树上路径颜色数(我不管,反正我真的没压行)。

#include<cstdio>
#include<cmath>
#include<algorithm>
//#define zczc
using namespace std;
const int N=100010;
inline void read(int &wh){
    wh=0;int f=1;char w=getchar();
    while(w<'0'||w>'9'){if(w=='-')f=-1;w=getchar();}
    while(w<='9'&&w>='0'){wh=wh*10+w-'0';w=getchar();}
    wh*=f;return;
}
int m,n,a[N],s[N<<1],head[N],esum;
struct edge{int t,nxt;}e[N<<1];
inline void add(int fr,int to){esum++;e[esum].t=to;e[esum].nxt=head[fr];head[fr]=esum;}
int lg[N],nxt[N][20],d[N],cnt,p1[N],p2[N];
void dfs(int wh,int fa,int deep){
	s[++cnt]=wh;p1[wh]=cnt;d[wh]=deep;nxt[wh][0]=fa;
	for(int i=1;i<=lg[d[wh]];i++)nxt[wh][i]=nxt[nxt[wh][i-1]][i-1];
	for(int i=head[wh],th;i;i=e[i].nxt){th=e[i].t;if(th==fa)continue;dfs(th,wh,deep+1);}
	s[++cnt]=wh;p2[wh]=cnt;
}
inline void swap(int &s1,int &s2){int s3=s1;s1=s2;s2=s3;return;}
int lca(int s1,int s2){
	if(d[s1]<d[s2])swap(s1,s2);
	for(int i=lg[d[s1]];i>=0;i--)
		if(d[nxt[s1][i]]>=d[s2])s1=nxt[s1][i];
	if(s1==s2)return s1;
	for(int i=lg[d[s1]];i>=0;i--)
		if(nxt[s1][i]!=nxt[s2][i])s1=nxt[s1][i],s2=nxt[s2][i];
	return nxt[s1][0];
}
struct node{
	int l,r,a,b,pl,ll;
}q[N];
int bl;
inline bool cmp(node s1,node s2){
	if(s1.l/bl==s2.l/bl)return s1.r<s2.r;
	else return s1.l/bl<s2.l/bl;
}
bool in[N];
int num[N],an[N],ans;
inline void push(int wh,int val){
	num[wh]+=val;
	if(val==1&&num[wh]==1)ans++;
	if(val==-1&&num[wh]==0)ans--;
}
inline void change(int wh){
	if(wh==0)return;
	int co=a[wh];
	if(in[wh])push(co,-1);else push(co,1);
	in[wh]=!in[wh];
}
signed main(){
	#ifdef zczc
	freopen("in.txt","r",stdin);
	#endif
	lg[0]=1;int s1,s2;
	for(int i=1;i<N;i++)lg[i]=lg[i>>1]+1;
	read(m);read(n);
	for(int i=1;i<=m;i++)read(a[i]);
	for(int i=1;i<=m;i++){read(s1);read(s2);if(s1==0||s2==0)continue;add(s1,s2);add(s2,s1);}
	dfs(1,0,1);
	for(int i=1;i<=n;i++){
		q[i].pl=i;read(s1);read(s2);read(q[i].a);read(q[i].b);int lc=lca(s1,s2);
		if(lc==s1||lc==s2){
			if(p1[s1]>p1[s2])swap(s1,s2);
			q[i].l=p1[s1];q[i].r=p1[s2];q[i].ll=0;
		}
		else{
			if(p1[s1]>p1[s2])swap(s1,s2);
			q[i].l=p2[s1];q[i].r=p1[s2];q[i].ll=lc;
		}
	}
	bl=ceil(sqrt(m*2))+1;sort(q+1,q+n+1,cmp);
	int l=0,r=0;
	for(int i=1;i<=n;i++){
		while(r<q[i].r)change(s[++r]);
		while(l>q[i].l)change(s[--l]);
		while(r>q[i].r)change(s[r--]);
		while(l<q[i].l)change(s[l++]);
		if(q[i].ll!=0)change(q[i].ll);
		if(q[i].a!=q[i].b&&num[q[i].a]!=0&&num[q[i].b]!=0)an[q[i].pl]=ans-1;
		else an[q[i].pl]=ans;
		if(q[i].ll!=0)change(q[i].ll);
	}
	for(int i=1;i<=n;i++)printf("%d\n",an[i]);
	return 0;
}
posted @ 2022-01-02 13:08  Feyn618  阅读(36)  评论(0编辑  收藏  举报