莫队算法入门

昨天重温了一下CaptainMo的职业生涯 莫队的模板,看了下别人的博客,把三个板子打了,做练习前先小小总结了一下吧。
一. 基础莫队算法
莫队算法 = 离线 + 暴力 + 分块,它通常用于不修改只查询的一类区间问题,复杂度为在这里插入图片描述
主要就是通过排序过后再处理询问能优化暴力,排序则是利用分块,至于为什么更优,附张别人博客看到的图。
在这里插入图片描述
这就很显然图二的走法更短,然后 编码时,还可以对排序做一个小优化:奇偶性排序,让奇数块和偶数块的排序相反。例如左端点L都在奇数块,则对R从大到小排序;若L在偶数块,则对R从小到大排序(反过来也可以:奇数块从小到大,偶数块从大到小)。
luogu P1972 HH的项链代码,这代码不能拿全分(不会卡常的屑 ),但正确性无误

#include<bits/stdc++.h>
const int N=1e6+10;
using namespace std;
int n,m,a[N],cnt[N],ans[N],block,id[N],t,L,R,sum;
struct ask
{
	int l;
	int r;
	int Id;
}q[N];
inline int Read()
{
	int x=0,f=1;
	char ch;
	while(ch>'9'||ch<'0')
	{
		if(ch=='-')	f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=x*10+ch-'0';
		ch=getchar();
	}
	return f*x;
}
bool comp(ask a,ask b)
{
	if(id[a.l]!=id[b.l]) return id[a.l]<id[b.l];
	if((id[a.l]%2)==1) return a.r>b.r;
	else return a.r<b.r; 
}
void add(int x)
{
	x=a[x];
	if(!cnt[x]) sum++;
	cnt[x]++;
}
void del(int x)
{
	x=a[x];
	cnt[x]--;
	if(!cnt[x]) sum--;
}
int main()
{
	n=Read();
	block=sqrt(n)+1;
	t=n/block;
	if(n%block) t++;
	for(int i=1;i<=n;i++) a[i]=Read(),id[i]=(i-1)/block+1;
	m=Read();
	for(int i=1;i<=m;i++)
	{
		q[i].l=Read(),q[i].r=Read();
		q[i].Id=i;
	}
	sort(q+1,q+1+m,comp);
	L=1,R=0;
	for(int i=1;i<=m;i++)
	{
		while(L<q[i].l) del(L),L++;
		while(L>q[i].l) L--,add(L);
		while(R<q[i].r) R++,add(R);
		while(R>q[i].r) del(R),R--;
		ans[q[i].Id]=sum;
	}
	for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
	return 0;
}

二. 带修改的莫队
如果是比较简单的“单点修改”,也能应用莫队算法,复杂度在这里插入图片描述
还是有道例题luogu P1903数颜色
如果用莫队算法求解,必须离线,先把查询操作和修改操作分别记录下来。记录查询操作的时候,增加一个变量,记录本次查询前做少次修改。
.

如果没有修改,就是基础莫队,一个查询的左右端点是[L, R]。加上修改之后,一个查询表示为(L, R, t),t表示在查询[L, R]前进行次修改操作。可以把t理解为“时间”,t的范围是1 ≤ t ≤ m,m是操作次数。
.
反正就是要多考虑修改操作对查询的影响,又因为是单点修改,可以直接暴力,查询中就多了这么一截

void CaptainMo()
{
	L=1,R=now=0;
	for(int i=1;i<=qnum;i++)
	{
		while(L<q[i].l) del(a[L]),L++;
		while(L>q[i].l) L--,add(a[L]);
		while(R<q[i].r) R++,add(a[R]);
		while(R>q[i].r) del(a[R]),R--;
		while(now>q[i].pre) update(now,i),now--;//需要还原修改
		while(now<q[i].pre) now++,update(now,i);//需要继续修改
		ans[q[i].Id]=sum;
	}
}

然后就是一些小细节的不同,比如分块大小应该为在这里插入图片描述
因为这样更快。排序时右端点r也按所在块的序号大小排,不是r大小。还有就是在修改时要注意写这句swap(a[x],p[pos].val);为什么的话模拟一下就知道了,当时理解了好久才看懂。
代码

#include<bits/stdc++.h>
using namespace std;
const int N=1400000;
int n,m,L,R,x,y,now,id[N],block,a[N],ans[N],cnt[N],qnum,pnum,sum;
struct ask
{
	int l;
	int r;
	int Id;
	int pre;
}q[N];
struct upd
{
	int st;
	int val;
}p[N];
inline int Read()
{
	int x=0,f=1;
	char ch;
	while(ch>'9'||ch<'0')
	{
		if(ch=='-')	f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=x*10+ch-'0';
		ch=getchar();
	}
	return f*x;
}
bool comp(ask A,ask b)
{
	if(id[A.l]!=id[b.l]) return id[A.l]<id[b.l];
	if(id[A.r]!=id[b.r]) return id[A.r]<id[b.r];
	return A.pre<b.pre;
}
void add(int x)
{
	if(!cnt[x]) sum++;
	cnt[x]++;
}
void del(int x)
{
	cnt[x]--;
	if(!cnt[x]) sum--;
}
void update(int pos,int i)
{
	int x=p[pos].st;
	if(x>=q[i].l&&x<=q[i].r)
	{
    	cnt[a[x]]--;
     	if(!cnt[a[x]]) sum--;
		if(!cnt[p[pos].val]) sum++;
		cnt[p[pos].val]++;		
	}
	swap(a[x],p[pos].val);
}
void CaptainMo()
{
	L=1,R=now=0;
	for(int i=1;i<=qnum;i++)
	{
		while(L<q[i].l) del(a[L]),L++;
		while(L>q[i].l) L--,add(a[L]);
		while(R<q[i].r) R++,add(a[R]);
		while(R>q[i].r) del(a[R]),R--;
		while(now>q[i].pre) update(now,i),now--;
		while(now<q[i].pre) now++,update(now,i);
		ans[q[i].Id]=sum;
	}
}
int main()
{
	n=Read(),m=Read();
	double X=pow(n,2.0/3.0);
	block=(int)X;
	for(int i=1;i<=n;i++) a[i]=Read(),id[i]=(i-1)/block+1;
	for(int i=1;i<=m;i++)
	{
		char s;
		cin>>s,x=Read(),y=Read();
		if(s=='Q')
		q[++qnum].l=x,q[qnum].r=y,q[qnum].Id=qnum,q[qnum].pre=pnum;
		else
		p[++pnum].st=x,p[pnum].val=y;
	}
	sort(q+1,q+1+qnum,comp);
	CaptainMo();
	for(int i=1;i<=qnum;i++) printf("%d\n",ans[i]);
	return 0;
}

三. 树上莫队
基础莫队和带修改的莫队操作的都是一维数组。基于其他的数据结构的问题,如果能转换成一维数组而且是区间问题,那么也能应用莫队算法。
典型的例子是树形结构上的路径问题,可以利用“欧拉序”把整棵树的结点顺序转化为一个一维数组,路径问题也变成了区间问题,就能利用莫队算法求解。下面还是有道例题。
SP10707 COT2 - Count on a tree II

相信学过树剖后对树上的节点存到队列已经熟悉,然后就考虑(u, v)上的路径有哪些结点?首先计算出u、v的lca(u, v)(最近公共祖先),然后讨论两种情况:
(1)lca(u, v) = u或lca(u, v) = v,这种情况最简单,可以直接做,不多说了。
(2)lca(u, v) ≠ u且lca(u, v) ≠ v,这种情况其实也不复杂,就在查询时多查询一个lca再计算答案,然后再查询一次消除lca对目前的sum值影响。
至于如何忽略掉区间内出现了两次的点,这个很简单,我们多记录一个vis[x],表示x这个点有没有被加入,每次处理的时候如果vis[x]=0则需要添加节点;如果vis[x]=1则需要删除节点。
代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+100;
int vis[N],L,R,cnt[N],f[40010][20],n,m,sum,b[N],a[N],len,ans[N],idx[N],Idx[N],ref[N],index_,id[N],block,dep[N],first[N],Next[N],to[N],tot;
struct ask
{
	int l;
	int r;
	int id;
	int zx;
}q[N];
int Read()
{
	int x=0,f=1;
	char ch=getchar();
	while(!isdigit(ch))
	{
		if(ch=='-') f=-1;
		ch=getchar();
	} 
	while(isdigit(ch))
	{
		x=(x<<1)+(x<<3)+ch-'0';
		ch=getchar();
	}
	return f*x;
}
void add(int a,int b)
{
	tot++;
	Next[tot]=first[a];
	to[tot]=b;
	first[a]=tot;
}
void dfs(int u,int fa)
{
	index_++;
	idx[u]=index_;
	ref[index_]=u;
	dep[u]=dep[fa]+1;
	for(int i=1;i<20;i++) f[u][i]=f[f[u][i-1]][i-1];
	for(int i=first[u];i;i=Next[i])
	{
		int v=to[i];
		if(v==fa) continue;
		f[v][0]=u;
		dfs(v,u);
	}
	Idx[u]=++index_,ref[index_]=u;
}
int lca(int u,int v)
{
	if(dep[u]<dep[v]) swap(u,v);
	for(int i=19;i>=0;i--) if(dep[f[u][i]]>=dep[v]) u=f[u][i];
	if(u==v) return u;
	for(int i=19;i>=0;i--) if(f[u][i]!=f[v][i]) u=f[u][i],v=f[v][i];
	return f[u][0];
}
bool comp(ask a,ask b)
{
	if(id[a.l]!=id[b.l]) return id[a.l]<id[b.l];
	if(id[a.l]%2==1) return a.r>b.r;
	else return a.r<b.r;
}
void update(int x)
{
	vis[x]^=1;
	if(vis[x]) 
	{
		cnt[a[x]]++;
		if(cnt[a[x]]==1) sum++;
	}
	else
	{
		cnt[a[x]]--;
		if(!cnt[a[x]]) sum--;
	}
}
void CaptainMo()
{
	L=1,R=0;
	for(int i=1;i<=m;i++)
	{
		while(L>q[i].l) L--,update(ref[L]);
		while(L<q[i].l) update(ref[L]),L++;
		while(R>q[i].r) update(ref[R]),R--;
		while(R<q[i].r) R++,update(ref[R]);
		if(q[i].zx) update(q[i].zx);
		ans[q[i].id]=sum;
		if(q[i].zx) update(q[i].zx);		
	}
}
int main()
{
	n=Read(),m=Read();
	block=(int)pow(n,2.0/3.0);
	for(int i=1;i<=n;i++) a[i]=b[i]=Read();
	for(int i=1;i<=2*n;i++ ) id[i]=(i-1)/block+1;
	sort(b+1,b+1+n);
	len=unique(b+1,b+1+n)-b-1;
	for(int i=1;i<=n;i++) a[i]=lower_bound(b+1,b+1+len,a[i])-b;
	for(int i=1;i<n;i++) 
	{
		int x=Read(),y=Read();
		add(x,y),add(y,x);
	}
	dfs(1,0);
//	for(int i=1;i<=index_;i++) cout<<ref[i]<<" ";
	for(int i=1;i<=m;i++)
	{
		int x=Read(),y=Read();
		int z=lca(x,y);
		if(z==x||z==y)
		{
			if(idx[x]>idx[y]) swap(x,y);
			q[i].l=idx[x],q[i].r=idx[y];
		}
		else
		{
			if(idx[x]>Idx[y]) swap(x,y);
			q[i].l=Idx[x],q[i].r=idx[y];
			q[i].zx=z;
		}
		q[i].id=i;
	}
	sort(q+1,q+1+m,comp);
	CaptainMo();
	for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
	return 0;
}

该去做练习了啊…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值