芝士:树上莫队

树上莫队

将区间的莫队算法拓展到树上,以此来解决一些树上路径的问题

思路

考虑为什么普通莫队为什么只是排一个序就可以把暴力的时间复杂度除以\(\sqrt n\)

其原因是保存了之前的结果,也就是指对于重复的部分不需要多余的空间,只需要调整询问的顺序就可以在整体上得到最大的优化。

树上莫队也是如此,但是我们要考虑如何将一颗树转换成序列的形式,并且所有的操作都需要变成对一个连续区间的询问

树上的路径在一般情况下可以分成二种,一种是一个节点是另一个节点的\(LCA\),另一种情况就是这两个节点的\(LCA\)是第三个点

两种情况都面临一个共同的困难,如何判断一个点是不是在路径上?欧拉序就派上了用场,如果一个点不在我们需要的路径上,那么我们对操作所转换成为的连续区间中一定包含两个这个点(先不考虑怎么转换的区间),但同时,我们所选择的区间一定也要包含目标路径,所有无用的节点也必须出现两次

以下的\(st[u]\)表示欧拉序中第一次出现\(u\)的位置,\(ed[u]\)表示欧拉序中第二次(其实也是最后一次)出现\(u\)的位置

假设\(st[u]<st[v]\)

如果是第一种情况,很明显的,选择\(st[u]\sim st[v]\)

如果是第二种情况,选择\(ed[u]\sim st[v]\),想象一下我们欧拉序是怎么构造出来的,因为\(lca\)一定在\(u\)的子树外,所以要选择\(ed[u]\),但同时的,考虑到不能包含到\(v\)节点子树内的节点,所以要选择\(st[u]\),但是同时我们会把\(LCA\)忽略掉,不过我们也只会忽略掉\(LCA\),考虑两条路径\(u\sim LCA\)\(LCA\sim v\),对于\(u\sim LCA\)这条路径,因为我们是在往上走,所以对于所有的有用的节点,只会出现1次,如果是其他节点,一定会先进入再出来,所以会出现两次,对于\(LCA\sim v\)这条路径也是一样的分析的方法,但是因为我们所有的操作都在以\(LCA\)为根的这棵子树内,所以在欧拉序中\(LCA\)的两次出现位置不会被包含

因为我们实际上把一个节点数为\(n\)的数变成了一个长度为\(2n\)的序列,同样的,块的大小要从\(\sqrt n\)变成\(\sqrt {2n}\)

所以时间复杂度依然是\(O(n\sqrt n)\)

例题

传送门

并没有什么特别的

#include<iostream>
#include<algorithm>
#include<vector>
#include<cstdio>
#include<cmath>
#include<map>
using namespace std;
int divi;
struct pr
{
	int l,r;
	int lca,id;
	friend bool operator < (const pr &a,const pr &b)
	{
		return (a.l/divi==b.l/divi?a.r<b.r:a.l<b.l);
	}
}p[100005];
int n,m,cnt,tot,cal;
int dp[100005][25],dep[100005],st[100005],ed[100005];
int val[100005],ans[100005],id[200005];
vector<int> g[100005];
map<int,int> h;
int l,r;
int t[100005];
bool used[100005];
void dfs(int u,int fa)
{
	st[u]=++cnt;
	id[cnt]=u;
	dep[u]=dep[fa]+1;
	for(int i=1;i<=20;i++)
		dp[u][i]=dp[dp[u][i-1]][i-1];
	for(int i=0;i<g[u].size();i++)
	{
		int v=g[u][i];
		if(v!=fa)	
		{
			dp[v][0]=u;
			dfs(v,u);
		}
	}
	ed[u]=++cnt;
	id[cnt]=u;
}
int lca(int u,int v)
{
	if(dep[u]>dep[v])
		swap(u,v);
	for(int i=20;i>=0;i--)
		if(dep[u]<=dep[dp[v][i]])
			v=dp[v][i];
	if(u==v)
		return u;
	for(int i=20;i>=0;i--)
		if(dp[u][i]!=dp[v][i])
		{
			u=dp[u][i];
			v=dp[v][i];
		}
	return dp[u][0];
}
void add(int pos)
{
	t[val[pos]]++;
	if(t[val[pos]]==1)
		cal++;
}
void del(int pos)
{
	t[val[pos]]--;
	if(t[val[pos]]==0)
		cal--;
}
void calc(int pos)
{
	used[pos]?del(pos):add(pos);
	used[pos]^=1;
}
int main()
{
	ios::sync_with_stdio(false);
	cin>>n>>m;
	divi=sqrt(2*n);
	for(int i=1;i<=n;i++)
	{
		cin>>val[i];
		if(h.count(val[i])==0)
			h[val[i]]=++tot;
		val[i]=h[val[i]];
	}
	for(int i=1,u,v;i<n;i++)
	{
		cin>>u>>v;
		g[u].push_back(v);
		g[v].push_back(u);
	}
	dfs(1,0);
	for(int i=1,u,v;i<=m;i++)
	{
		cin>>u>>v;
		if(st[u]>st[v])
			swap(u,v);
		int t=lca(u,v);
		if(u==t)
		{
			p[i].l=st[u];
			p[i].r=st[v];
			p[i].lca=0;
		}
		else
		{
			p[i].l=ed[u];
			p[i].r=st[v];
			p[i].lca=t;
		}
		p[i].id=i;
	}
	sort(p+1,p+m+1);
	l=r=cal=1;
	t[val[id[1]]]++;
	used[val[id[1]]]=1;
	for(int i=1;i<=m;i++)
	{
		while(r<p[i].r)
		{
			r++;
			calc(id[r]);
		}
		while(p[i].r<r)
		{
			calc(id[r]);
			r--;
		}
		while(l<p[i].l)
		{
			calc(id[l]);
			l++;
		}
		while(p[i].l<l)
		{
			l--;
			calc(id[l]);
		}
		if(p[i].lca)
			calc(p[i].lca);
		ans[p[i].id]=cal;
		if(p[i].lca)
			calc(p[i].lca);
	}
	for(int i=1;i<=m;i++)
		cout<<ans[i]<<'\n';
	return 0;
}

posted @ 2020-09-11 20:21  loney_s  阅读(188)  评论(0)    收藏  举报