与图论的邂逅09:树上启发式合并

启发式树上合并

先看这样一个例题:

给定一个长度为\(10^5\)的序列,序列中都是\([0,1000000]\)的整数,接下来有\(10^5\)次询问,共两种询问:修改序列某个位置的值,以及查询区间\([L,R]\)内一共有多少种不同的数。

先不考虑线段树的做法。我们可以想到用莫队来解这道题———按莫队的套路排序后用桶来维护即可,至多1000000个桶,复杂度\(O(N\sqrt{N})\)。可以看到,我们的核心思路就是排序后改变了每个位置的访问顺序,从而降低了复杂度。

然后我们看到下面的例题(如果上面不是为了引出dsu我会乱说?):

给定一棵大小为\(10^6\)的树,树上每个点都是一个\([0,1000000]\)的整数,接下来有\(10^5\)次询问,查询某棵子树内一共有多少种不同的数。

树上莫队?不过我现在需要\(NlogN\)的算法。那么我们参考一下莫队的思想,可不可以通过修改访问顺序来降低复杂度?这个算法叫“树上启发式合并”,一般写\(dsu\)

考虑暴力的做法,就是暴力处理出所有子树的答案,再\(O(1)\)回答。递归遍历整棵树,然后对于每个点\(u\),暴力计算\(u\)子树的答案。复杂度为\(O(N^2)\)

在我们计算完\(u\)的儿子\(v\)的答案后,我们需要计算\(v\)的答案对\(u\)的贡献,然后把\(v\)的答案删去。于是我们发现,最后一棵子树的答案是不需要删除的。对比我们人的思维,我们肯定会把大的子树放到最后处理。然后我们发现了另一个优美的东西:重链剖分。由于重链剖分的做法,我们每次往轻儿子走之后,树的大小都会减少至少\(\frac{1}{2}\),所以每个节点到根的路径上至多有\(\log_2n\)条轻边,进而得出每个点被轻边连接的祖先最多遍历\(\log_2n\)次。那么我们把重子树放到最后处理,优先走轻边,边走边暴力处理轻子树的答案的话,那么任意重儿子到根的路径中的所有重边连接的祖先在计算完它们的轻子树的答案前是不会往下遍历到这个点的,所以一个点被遍历的次数就等于\(log_2n+1\)。复杂度就是\(NlogN\)

总结起来说,算法流程基本就是:\(O(n)\)重链剖分,\(O(nlogn)\)树上启发式合并,其中每遍历到一个点时先处理轻子树的答案,用轻子树的信息更新自己的答案,然后删掉轻子树的信息,遍历重儿子。最后\(O(m)\)回答。


这里有一个例题:

CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths

一棵根为1 的树,每条边上有一个字符(a-v共22种)。一条简单路径被称为Dokhtar-kosh当且仅当路径上的字符经过重新排序后可以变成一个回文串。求每个子树中最长的Dokhtar-kosh路径的长度。

\(1{\leq}n{\leq}5·10^5\)


很明显的性质是:回文路径上出现次数为奇数的字符至多只有一个。那么我们可以状压,设\(i\)表示路径上字符出现次数的奇偶状态,那么至多只有22种情况,枚举即可。

由于要求最长路径的长度,我们设\(u\)\(v\)的祖先,并且u到根的路径的奇偶状态与v相同,那么\(v\)显然优于\(u\),因为\(v\)的深度更大。那么我们设一个数组\(cnt_i\)表示路径状态为\(i\)的最大深度。

如何得到一条路径的状态?设路径的两端点为\(u,v\),那么状态就是\(state_u\)$state_v$\(state_{lca}\)^{state_{lca}},其中\(state_x\)表示\(x\)到根的路径的奇偶状态。

那么我们的算法就是:先\(O(n)\)重链剖分,并且处理出\(state\)数组;然后\(dsu\)。时间复杂度为\(O(nlogn)\)

#include<iostream>
#include<cstring>
#include<cstdio>
#define maxn 500010
using namespace std;

int dep[maxn],son[maxn],size[maxn],ldfn[maxn],rdfn[maxn],id[maxn],dfn;
int col[maxn],cnt[(1<<22)-1],state[maxn],ans[maxn];
int n,nowson;

struct edge{
	int to,col,next;
}e[maxn<<1];
int head[maxn],k;

inline void add(const int &u,const int &v,const int &w){
	e[k]=(edge){v,w,head[u]},head[u]=k++;
}

void dfs_getson(int u,int fa){
	size[u]=1,ldfn[u]=++dfn,id[dfn]=u;
	for(register int i=head[u];~i;i=e[i].next){
		int v=e[i].to;
		if(v==fa) continue;
		dep[v]=dep[u]+1,state[v]=state[u]^e[i].col,dfs_getson(v,u),size[u]+=size[v];
		if(size[v]>size[son[u]]) son[u]=v;
	}
	rdfn[u]=dfn;
}

void dsu(int u,int fa,bool op){
	for(register int i=head[u];~i;i=e[i].next){
		int v=e[i].to;
		if(v==fa||v==son[u]) continue;
		dsu(v,u,false),ans[u]=max(ans[u],ans[v]);
	}
	if(son[u]) dsu(son[u],u,true),ans[u]=max(ans[u],ans[son[u]]);
	if(cnt[state[u]]) ans[u]=max(ans[u],cnt[state[u]]-dep[u]);
	for(register int i=0;i<22;++i) if(cnt[state[u]^(1<<i)])
		ans[u]=max(ans[u],cnt[state[u]^(1<<i)]-dep[u]);
	cnt[state[u]]=max(cnt[state[u]],dep[u]);
	for(register int i=head[u];~i;i=e[i].next){
		register int v=e[i].to;
		if(v==fa||v==son[u]) continue;
		for(register int j=ldfn[v];j<=rdfn[v];++j){
			register int w=id[j];
			if(cnt[state[w]]) ans[u]=max(ans[u],cnt[state[w]]+dep[w]-2*dep[u]);
			for(register int k=0;k<22;++k) if(cnt[state[w]^(1<<k)])
				ans[u]=max(ans[u],cnt[state[w]^(1<<k)]+dep[w]-2*dep[u]);
		}
		for(register int j=ldfn[v];j<=rdfn[v];++j) cnt[state[id[j]]]=max(cnt[state[id[j]]],dep[id[j]]);
	}
	if(!op) for(register int i=ldfn[u];i<=rdfn[u];++i) cnt[state[id[i]]]=0;
}

int main(){
	memset(head,-1,sizeof head);
	cin>>n;
	for(register int i=2;i<=n;++i){
		int to; char col;
		cin>>to>>col;
		add(to,i,1<<(col-'a')),add(i,to,1<<(col-'a'));
	}
	dep[1]=1,dfs_getson(1,-1),dsu(1,-1,true);
	for(register int i=1;i<=n;++i) printf("%d ",ans[i]);
	return 0;
}
posted @ 2019-09-17 00:33  修电缆的建筑工  阅读(206)  评论(0编辑  收藏  举报