初探莫队

2019年的某月某天某神仙讲了莫队,但是我一直咕咕咕到了2020年

什么是莫队

莫队是一种优雅的暴力,也是用来完成区间询问的。普通莫队复杂度\(O(n \sqrt n)\)。一种十分优美的离线做法

前置芝士

0.拥有脑子

1.\(STL\)\(sort\)\(cmp\)

2.看/写超长的三目运算符的耐心

3.分块的思想

当然了如果不会这些也没有关系,下面还会再讲的

正片开始

先来一道卡了莫队的莫队模板题
HH的项链


最最暴力的做法:显然我们可以对每个询问暴力跑一次,但显然\(O(n^2)\)跑不起。
在上面的暴力中,我们浪费了大量之前遍历过的区间的信息,现在考虑利用起这些信息。我们可以设置两个指针\(l,r\)表示当前所处的区间左端点和右端点。初始化\(l=1,r=0\)。(为了避免某些神奇的\(RE\))如果\(l,r\)不与询问区间的端点重合,就不断的跳\(l,r\)来更新答案。如果\(l\)在左端点右边,就不断向左跳,同时将\(l\)跳到的数统计进答案中,直到与左端点重合。如果\(l\)在左端点左边,就不断往右跳,同时将曾经待过的点从答案中删掉。对于这个题来说,可以用\(cnt[x]\)表示\(x\)这个数出现的次数,如果某次增加时,\(cnt[x]==0\),\(ans\)\(+1\),如果某次删除时发现删完后\(cnt[x]==0\),\(ans\)\(-1\)

我们发现上面这个优化对于这种图来说效率极高:

其中\(x_i\)表示第\(i\)次询问对应的区间

但是对于这种数据来说就凉了

上面的优化方式在\(x_4\)里面不断得左右来回跳,导致浪费了大量的时间。

所以我们不妨把询问的区间进行排序。这样做就必须离线了。怎么排序呢?按照左端点单调递增?显然右端点无序会让这个优化只增加\(O(nlogn)\)的排序复杂度。这时候,就要用到分块思想了。

我们把整个序列分成\(\sqrt n\)个块,按照\(l\)所在的块升序排列为第一关键字,\(r\)升序排列为第二关键字排序。感觉好像没有什么用诶?但确实是个极大的优化至于为什么我也不知道

代码如下:

struct Q{
   int l,r,id,nub;//nub表示左端点在哪个块里
}qry[200009];
bool cmp(Q a,Q b)
{
	if(a.nub!=b.nub) return a.nub<b.nub;
	return a.r<b.r; 
}

当然卡常一点也可以写成这样:

bool cmp(Q a,Q b)
{ 
   return (a.nub^b.nub)?a.nub<b.nub:a.r<b.r;
}

过莫队板子的必备技能是卡常
这样基本的莫队就撒花完结了。

因为这道板子题卡了莫队,所以请走数据弱化版D_QUERY
板子题代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<vector>
#include<map>
#include<queue>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
inline ll read()
{
	char ch=getchar();
	ll x=0;bool f=0;
	while(ch<'0'||ch>'9')
	{
		if(ch=='-') f=1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=(x<<3)+(x<<1)+(ch^48);
		ch=getchar();
	}
	return f?-x:x;
}
int n,q,a[30009],ans[200009],cnt[1000009],all;
struct Q{
	int l,r,nub,id;
}qry[200009];
bool cmp(Q a,Q b)
{
	if(a.nub!=b.nub) return a.nub<b.nub;
	return a.r<b.r; //由于这题不卡常所以就没有卡
}
void add(int k)
{
	if(!cnt[a[k]]) all++;
	cnt[a[k]]++;
}
void del(int k)
{
	cnt[a[k]]--;
	if(!cnt[a[k]]) all--;
}
int main()
{
	n=read();
	for(int i=1;i<=n;i++)
	 a[i]=read();
	q=read();
	int sn=sqrt(n);
	for(int i=1;i<=q;i++)
	{
		qry[i].id=i;qry[i].l=read();qry[i].r=read();
		qry[i].nub=qry[i].l/sn+1;
		if(qry[i].l%sn==0) qry[i].nub--;
	} 	
	sort(qry+1,qry+1+q,cmp);
	int l=1,r=0;
	for(int i=1;i<=q;i++)
	{
		while(r<qry[i].r) add(++r);
		while(r>qry[i].r) del(r--);
	    while(l<qry[i].l) del(l++);
	    while(l>qry[i].l) add(--l);
	    ans[qry[i].id]=all;
	}  
	for(int i=1;i<=q;i++)
	 printf("%d\n",ans[i]);
}

莫队的玄学优化

奇偶性排序

虽然上面的排序方法优化很大,但是能不能更快一点以便卡过毒瘤题呢?
方法当然是有的辣。
我们先来康康按照上面的排序方法会排出来个啥

这是一堆询问区间以及并不优美的块的分界线
排序后:

这样左端点跳动幅度不大,右端点在同一个块内也是递增的。但是当\(r\)从一个块跳到下一个块的时候发现有时候会倒退回来好多,然后又要重新向右跳。是不是有点浪费?所以奇偶性排序就是在奇数块内右端点按升序排序,偶数块内右端点按降序排序,这样右端点在往回跳的时候就能顺带跳完偶数块的询问。理论上能快一半

上面的按照奇偶性排序:

手动模拟\(r\)的跳跃发现真的优化了不少
代码:

bool cmp(Q a,Q b)
{
  return (a.nub^b.nub)?(a.nub<b.nub):((a.nub%2)?a.r<b.r:a.r>b.r);
}
乱七八糟系列

\(pragma\ GCC\ optimize(2),pragma\ GCC\ optimize (3),register\),快读快输,\(inline\),把\(for\)里的\(i++\)换成\(++i\),用三目运算符代替blabla(待会卡带修莫队板子要用)

带修莫队

现在毒瘤出题人要求修改,怎么办呢?
就像这道题:数颜色


在很久很久以前,这道题是可以拿树套树卡过的你甚至只用去搞搞set,但是现在拿带修莫队都要吸氧了\(QAQ\)

好了我们回到正题。
我们只需要在原来的莫队的基础上再加一维时间轴。将询问和修改分开存储。如果这次询问的时间在当前时间之后,就不断修改,直到时间相同。如果询问时间在当前时间之前,就再改回去,我们可以用\(swap\)做到,从而不用再开变量维护原来的值。

当然了,排序方式也有变化。这次我们按照\(l\)所在的块为第一关键字,\(r\)所在的块为第二关键字,时间为第三关键字进行排序。同时,奇偶性排序也不再适用。
排序:

bool cmp(Q a,Q b)
{
    return (bl[a.l]^bl[b.l])?bl[a.l]<bl[b.l]:((bl[a.r]^bl[b.r])?bl[a.r]<bl[b.r]:a.ti<b.ti);
}

注意块的大小会对复杂度有着极大的影响。据大佬证明当块的大小为\(n^{\frac{3}{4}}\)时,复杂度最优。

由于这个题窝太菜了,不拿\(O_2\)实在是卡不过去,所以只好放上一份加\(O_2\)的代码了

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<vector>
#include<map>
#include<queue>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
inline int read()
{
	char ch=getchar();
	int x=0;bool f=0;
	while(ch<'0'||ch>'9')
	{
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=(x<<3)+(x<<1)+(ch^48);
		ch=getchar();
	}
	return f?-x:x;
}
int n,k,q,a[133339],bl[133339],ans[133339],cnt[1000009];
int all;
struct Q{
	int l,r,ti,id;
}qry[133339];
struct M{
	int p;
	int col;
}mdi[133339];
bool cmp(Q a,Q b)
{
    return (bl[a.l]^bl[b.l])?bl[a.l]<bl[b.l]:((bl[a.r]^bl[b.r])?bl[a.r]<bl[b.r]:a.ti<b.ti);
}
inline void add(int k)
{
    if(!cnt[a[k]]) all++;
	cnt[a[k]]++;
}
inline void del(int k)
{
	cnt[a[k]]--;
    if(!cnt[a[k]]) all--;
}
inline void modi(int i,int ti)
{
	if(mdi[ti].p>=qry[i].l&&mdi[ti].p<=qry[i].r)
	{
		int x=--cnt[a[mdi[ti].p]];
		int y=++cnt[mdi[ti].col];
	    if(!x) all--;
	    if(y==1) all++;
	}
	swap(a[mdi[ti].p],mdi[ti].col);
}
int main()
{
	n=read();q=read();
	for(int i=1;i<=n;i++)
	 a[i]=read();
	int qc=0,mc=0;
    for(int i=1;i<=q;i++)
    {
    	char k=getchar();
    	while(k!='Q'&&k!='R') k=getchar();
    	if(k=='Q')
    	{
    		qry[++qc].l=read();qry[qc].r=read();
            qry[qc].ti=mc;qry[qc].id=qc;
		}
		if(k=='R')
		{
			mdi[++mc].p=read();mdi[mc].col=read();
		}
	}	
    int sn=pow(n,3.0/4.0);
	for(int i=1;i<=n;i++)
	{
		bl[i]=(i-1)/sn+1;
	}
	sort(qry+1,qry+1+qc,cmp);
    int now=0,l=1,r=0;
    for(int i=1;i<=qc;i++)
    {
    	while(r<qry[i].r) add(++r);
    	while(r>qry[i].r) del(r--);
    	while(l<qry[i].l) del(l++);
    	while(l>qry[i].l) add(--l);
    	while(now<qry[i].ti) modi(i,++now);//带修莫队只是多了这两个修改操作
    	while(now>qry[i].ti) modi(i,now--);
    	ans[qry[i].id]=all;
	}
    for(int i=1;i<=qc;i++)
     printf("%d\n",ans[i]);
}

莫队可以处理区间上的东西,而\(dfs\)序这种东西可以把树转化成区间,那么莫队可不可以解决树上的问题呢?

树上莫队

我们以这道题SP10707 COT2 - Count on a tree II为例,来看看树上莫队。

样例:

把样例画出来:

显然我们需要\(dfs\)序来把这棵树变成一个序列。
普通的\(dfs\)序: 1 2 3 5 6 7 4 8
2到5的路径:2 1 3 5
可以乱搞的区间去哪里了???
显然,普通的\(dfs\)序不能用莫队进行乱搞,所以,我们需要一种特殊的\(dfs\)序。

欧拉序

欧拉序是一种特殊的\(dfs\)序,当遍历到一个点时,将它加入\(dfs\)序中,再遍历它的子树。当它的子树遍历完时再将它加入到\(dfs\)序中。
煮个栗子:

这个图中,欧拉序是 1 2 2 3 5 5 6 6 7 7 3 4 8 8 4 1
可以看出每个点都会出现两遍,而且这两遍中间的所有点都是它的子树里的节点。这样有什么优点呢?
我们再来找找2到5的路径(2 1 3 5):
1呢?1被吃了。1作为2,5的\(lca\),在欧拉序中1在2的前面所以1被吃了
我们先存一下这个即将成为历史遗留问题的问题,看一下具有祖孙关系的节点之间的路径怎么求。
炒个栗子:
1到6的路径:1 3 6

emmm也许我们又多了一个历史遗留问题
so我们应该怎么找路径对应的区间解决历史遗留问题呢?

我们可以记录每个点\(i\)在欧拉序中第一次出现的位置\(first[i]\)(以下简写为\(fst[i]\))和最后一次出现的位置\(last[i]\)(以下简写为\(lst[i]\))。我们现在要找\(u\)\(v\)的路径对应的区间(这里假设\(fst[u]<fst[v]\),不满足就\(swap\)),如果\(lca(u,v)==u\),就是\([fst[u],fst[v]]\)这段区间的答案,否则,是\([lst[u],fst[v]]\)这段区间的答案。\(why?\)打表可得因为\(lca\)\(u\)\(v\)的上面,所以从\(u\)走到\(v\)一定是回溯完\(u\)才能到\(v\),所以是区间左端点是\(lst[u]\)而不是\(fst[u]\)

我们注意到按照上面的找区间方法并没有解决历史遗留问题1中的处理\(lca\),同时也有可能会在确定的区间中发现某些节点出现了两次。所以我们应该特殊处理一下。第奇数次走到某个节点,就进行类似\(add\)的操作,而第偶数次遍历到某个节点就进行类似\(del\)的操作,可以消除区间中出现两次的点(实际上在树上并没有经过的点)的影响。对于\(lca\),我们单独进行上面所说的操作,统计完答案后再操作一次来消除这次操作对后面的影响。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<vector>
#include<map>
#include<queue>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int inf=214748364;
inline int read()
{
	char ch=getchar();
	int x=0;bool f=0;
	while(ch<'0'||ch>'9')
	{
		if(ch=='-') f=1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9')
	{
		x=(x<<3)+(x<<1)+(ch^48);
		ch=getchar();
	}
	return f?-x:x;
}
int n,m,a[40009],cnt,head[40009],fst[40009],lst[40009];
int qwq[80009],q,tot,dep[40009],f[40009][21],bl[80009];
int b[40009],w,ans[100009],all,num[40009];
bool vis[80009];
struct E{
	int to,nxt;
}ed[80009];
struct Q{
	int l,r,id,lca;
}qry[100009];
void add(int fr,int to)//这个是存边的add,不是维护答案的add
{
	ed[++cnt].to=to;
	ed[cnt].nxt=head[fr];
	head[fr]=cnt;
}
void dfs(int now,int fa)
{
	dep[now]=dep[fa]+1;
	qwq[++tot]=now;fst[now]=tot;
	f[now][0]=fa;
	for(int e=head[now];e;e=ed[e].nxt)
	{
		int v=ed[e].to;
		if(v==fa) continue;
		dfs(v,now);
	}
	qwq[++tot]=now;lst[now]=tot;
}
int Lca(int u,int v)
{
	if(dep[u]<dep[v]) swap(u,v);
	for(int i=20;i>=0;i--)
	 if(dep[f[u][i]]>=dep[v]) u=f[u][i];
    if(u==v) return u;
    for(int i=20;i>=0;i--)
    {
    	if(f[u][i]!=f[v][i]) u=f[u][i],v=f[v][i];
	}
	return f[u][0];
}
//以上是倍增搞lca
int fd(int k)
{
	int l=1,r=w;
	while(l<=r)
	{
		int mid=(l+r)>>1;
		if(b[mid]==k) return mid;
		if(b[mid]>k) r=mid-1;
		else l=mid+1;
	} 
	while(b[l]>k) l--;
	return l;
}
//由于数据过大,进行离散化
bool cmp(Q a,Q b)
{
	return (bl[a.l]^bl[b.l])?(bl[a.l]<bl[b.l]):((bl[a.l]%2)?a.r<b.r:(a.r>b.r));
}
void deal(int k)//对区间进行的操作
{
	(vis[k])? all-=!(--num[a[k]]) :all+= !num[a[k]]++;
	vis[k]^=1;
}
int main()
{
	n=read();m=read();
	for(int i=1;i<=n;i++)
	 a[i]=read(),b[i]=a[i];
	for(int i=1;i<=n-1;i++)
	{
		int fr=read(),to=read();
		add(fr,to);add(to,fr);
	} 
	sort(b+1,b+1+n);
	w=unique(b+1,b+1+n)-b-1;
	for(int i=1;i<=n;i++)
	 a[i]=fd(a[i]);
	dfs(1,0);
	for(int j=1;j<=20;j++)
	 for(int i=1;i<=n;i++)
	  f[i][j]=f[f[i][j-1]][j-1];
	for(int i=1;i<=m;i++)
	{
		qry[i].id=i;int u=read(),v=read();
        if(fst[u]>fst[v]) swap(u,v);	    
		int lca=Lca(u,v);
	    if(lca==u) qry[i].l=fst[u],qry[i].r=fst[v],qry[i].lca=0;
	    else qry[i].l=lst[u],qry[i].r=fst[v],qry[i].lca=lca;
	}	
	int sn=sqrt(2*n);
	for(int i=1;i<=2*n;i++)
	 bl[i]=(i-1)/sn;
	sort(qry+1,qry+1+m,cmp); 
	int l=1,r=0;
	for(int i=1;i<=m;i++)
	{
		while(r<qry[i].r) deal(qwq[++r]);
		while(r>qry[i].r) deal(qwq[r--]);
		while(l<qry[i].l) deal(qwq[l++]);
		while(l>qry[i].l) deal(qwq[--l]);
	    if(qry[i].lca) deal(qry[i].lca);
	    ans[qry[i].id]=all;
	    if(qry[i].lca) deal(qry[i].lca);
	}
	for(int i=1;i<=m;i++)
	 printf("%d\n",ans[i]);
}
posted @ 2020-01-04 21:29  千载煜  阅读(198)  评论(2编辑  收藏  举报