支配树学习笔记

一、引入

本篇博客参考了这位大佬的博客,写的非常好,让蒟蒻受益匪浅。

支配树是个什么东西呢?

首先我们定义支配点:在一个有向图上,指定一个起点\(S\),然后如果删除一个点\(x\)以及他连接的所有边,就无法再从\(S\)到达另一个点\(y\),那么\(x\)就是\(y\)的支配点。

而支配树,则是一棵满足树上任意一个点的所有祖先都是它的支配点的树。

显然一个点能支配的所有点就是它在支配树上的子树中的点。同时,我们特别定义一个点\(x\)在支配树上的父亲为\(idom(x)\)

构建支配树,显然可以用暴力枚举断掉每一个点后再\(bfs\)一遍完成,但这样做是\(\mathcal O(nm)\)的,我们需要更高效的算法。

二、DAG上构建支配树

我们先研究简化版的情况:\(DAG\)上的支配树。

\(DAG\)上,考虑如果\(y\)是点\(x\)的支配点,那么需要满足断掉\(y\)后,从\(S\)无法到达所有可以到达\(x\)的点\(k\),也就是说,\(y\)是所有\(k\)的支配点,也就是这些\(k\)在支配树上的公共祖先,那么它们中深度最大的一个,也就是这些\(k\)\(LCA\),就是\(idom(x)\)

于是我们按拓扑序从小到大依次处理,那么当处理到\(x\)时,所有可达\(x\)的点一定都已经被加入到支配树中了,就可以\(\mathcal O(log(n))\)得到\(idom(x)\)

例题1:洛谷P2597 [ZJOI2012]灾难

捕食者的所有食物死亡时它就会死亡,因此导致它死亡的就是反向建图后它的支配点。

于是反向建图并建出支配树后,每种生物的灾难值就是它的子树大小\(-1\),同时对于最低级生物有多种的情况,我们考虑再新建一种生物作为所有最低级生物的食物,然后以它作为起点。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,first[N],cnt,rt[N],top[N],tot,d[N];
struct node{
	int v,nxt;
}e[N<<1];
vector<int> ru[N];
inline void add(int u,int v){e[++cnt].v=v;e[cnt].nxt=first[u];first[u]=cnt;ru[v].push_back(u);}
namespace dt{
	int dep[N],pa[N][20],tot,head[N],siz[N];
	node d[N<<1];
	inline void Add(int u,int v){
		dep[v]=dep[u]+1;
		pa[v][0]=u;
		for(int i=1;i<=19;++i) pa[v][i]=pa[pa[v][i-1]][i-1];
		d[++tot].v=v;d[tot].nxt=head[u];head[u]=tot;
	}
	inline int LCA(int u,int v){
		if(dep[u]<dep[v]) swap(u,v);
		int t=dep[u]-dep[v];
		for(int i=19;i>=0;--i) if(t&(1<<i)) u=pa[u][i];
		if(u==v) return u;
		for(int i=19;i>=0;--i) if(pa[u][i]!=pa[v][i]) u=pa[u][i],v=pa[v][i];
		return pa[u][0];
	}
	inline void dfs(int u){
		siz[u]=1;
		for(int i=head[u];i;i=d[i].nxt){
			int v=d[i].v;
			if(v!=pa[u][0]) dfs(v),siz[u]+=siz[v];
		}
	}
	inline void build(){
		for(int i=2;i<=n;++i){
			int u=top[i],lca=0;
			for(int j=0;j<ru[u].size();++j){
				int v=ru[u][j];
				if(!lca) lca=v;
				else lca=LCA(lca,v);
			}
			Add(lca,u);
		}
		dfs(n);
		for(int i=1;i<n;++i) printf("%d\n",siz[i]-1);
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		int x;
		scanf("%d",&x);
		if(!x) add(n+1,i);
		else
			do{add(x,i);}while(scanf("%d",&x)&&x);
	}
	++n;
	queue<int> q;q.push(n);
	for(int i=1;i<=n;++i) d[i]=ru[i].size();
	while(!q.empty()){
		int u=q.front();q.pop();
		top[++tot]=u;
		for(int i=first[u];i;i=e[i].nxt){
			int v=e[i].v;
			if(!(--d[v])) q.push(v);
		}
	}
	dt::build();
	return 0;
}

例题2:CF757F Team Rocket Rises Again

solution

三、有向图的支配树

接下来我们介绍一个优秀的解决支配树问题的算法——Lengauer Tarjan算法

首先我们建出原图的\(dfs\)树并求出每个点的\(dfs\)

为了证明一些性质,我们定义\(a\cdot \rightarrow b\)表示\(a\)\(b\)的祖先,\(a+\rightarrow b\)表示\(a\)\(b\)的祖先且\(a\not=b\),并且一下所有点之间的比较都代表它们\(dfs\)序间的比较。

\(dfs\)树对于我们的目标来说有两个重要的性质:

  • 性质\(1\):横叉边只从\(dfn\)较大的点连向\(dfn\)较小的点。

    证明:如果不满足,那么\(dfs\)时直接会从这条边走,于是这条边就不会是横叉边了

  • 性质\(2\):如果存在两个点\(u,v\)满足\(u\le v\),那么任意\(u\rightarrow v\)的路径一定经过\(u,v\)的公共祖先

    证明:如果\(u,v\)是祖先关系显然成立,否则删掉\(u,v\)的所有公共祖先,那么\(u,v\)被分离在两个子树中,\(u\rightarrow v\)要跨越子树,能跨越子树的边只有横叉边,而它只能从\(dfn\)较大的点走向\(dfn\)较小的点,于是\(u\)无法到达\(v\),因此\(u\rightarrow v\)的路径一定经过公共祖先。

接着我们引入一个新的概念:

半支配点

对于任意两点\(x,y\),如果存在一条从\(y\)出发到达\(x\)的路径且路径上除\(x,y\)以外的任意一点\(k\)都满足\(dfn[k]>dfn[x]\),且\(y\)是所有对\(x\)满足这一性质的点中\(dfn\)最小的一个,就称\(y\)\(x\)的半支配点,即\(semi(x)\)

重要引理:

  • 引理\(1\)\(semi(x)+ \rightarrow x\)

    证明:\(x\)\(dfs\)树上的父亲\(fa_x\)一定也满足\(semi\)的性质,所以一定有\(semi(x)\le fa_x\),又因为\(semi(x)\)不可能在其他子树上,因为由性质\(2\)我们知道,\(semi(x)\rightarrow x\)的路径一定经过公共祖先\(w\)\(w<semi(x)\),与定义矛盾,因此\(semi(x)\)一定是\(fa_x\)的祖先。

  • 引理\(2\)\(idom(x)\cdot \rightarrow semi(x)\)

    证明:如果引理\(2\)不成立,那么从\(semi(x)\)\(x\)的路径就能绕开\(idom(x)\),与定义不符。

  • 引理\(3\):任意满足\(x\cdot \rightarrow y\)的点\(x,y\)都有\(x\cdot \rightarrow idom(y)\)\(idom(y)\cdot \rightarrow idom(x)\)

    证明:如果不成立,则\(idom(x)+\rightarrow idom(y)+\rightarrow x+\rightarrow y\),于是\(idom(y)\)不支配\(x\),那么存在路径绕过\(idom(y)\)到达\(x\),进而到达\(y\),与定义不符,所以引理\(3\)成立

求解半支配点

对于点\(x\),我们找到所有连向\(x\)的点\(y\)

  • 如果\(dfn[y]<dfn[x]\),那么\(y\)满足半支配点的性质,直接更新
  • 否则,我们用\(y\)的所有祖先\(z\)\(semi(z)\)来更新\(semi(x)\)

感性理解一下:

  • 首先\(semi(z)\)一定存在路径可以绕过\(semi(z)\)\(x\)之间的点到达\(x\),于是\(semi(x)\le semi(z)\)

  • 其次,考虑\(semi(x)\)\(x\)的路径,如果只有一条边,那么会由情况\(1\)更新,否则,我们取\(y\)\(x\)的前驱,再取点\(z\)为满足\(z\cdot \rightarrow y\)\(z\)不是两端的点的最小\(z\),(一定存在这样的一个点,因为\(y\)自己就是一个符合条件的\(z\)),那么\(semi(x)\)\(z\)的路径一定绕过了\(z\)的祖先,于是\(semi(x)\)\(semi(z)\)的候选点,有\(semi(x)\ge semi(z)\)

  • 综上所述,我们可以用\(semi(z)\)来更新\(semi(x)\)且没有其他情况。

  • 对此,我们可以用带权并查集维护每个点在\(dfs\)树上到根路径上满足\(dfn(semi(z))\)最小的\(z\),然后用它来更新\(semi(x)\)

  • 代码如下:\(rg\)是反图

inline int find(int x){//带权并查集,mi[x]是x到根路径上满足dfn[semi[k]]最小的k 
	if(f[x]==x) return x;
	int t=f[x];f[x]=find(f[x]);
	if(dfn[semi[mi[t]]]<dfn[semi[mi[x]]]) mi[x]=mi[t];
	return f[x];
}//路径压缩后用路径上的semi更新当前点维护的mi 
inline void findidom(){
	for(int i=1;i<=n;++i) mi[i]=semi[i]=f[i]=i;
	for(int i=n;i>=2;--i){//按dfs序倒序枚举 
		int u=pos[i],sem=n;
		for(int j=rg.first[u];j;j=rg.e[j].nxt){
			int v=rg.e[j].v;
			if(dfn[v]<dfn[u]) sem=min(sem,dfn[v]);//dfn[v]<dfn[u]直接更新semi
			else find(v),sem=min(sem,dfn[semi[mi[v]]]);//否则,用v祖先中semi最小的一个更新semi
		}
		semi[u]=pos[sem];f[u]=fa[u];//找到semi[u],将它并入并查集中 
    }
}

用半支配点求解支配点

假设我们已知\(semi(x)\),\(x \not= s\),需要求解的是\(idom(x)\)

\(P\)\(semi(x)\)\(x\)的所有路径的点集(不包含\(semi(x)\)),取\(z\)\(P\)中满足\(dfn(semi(z))\)最小的点,我们有以下定理:

  • 定理\(1\):如果\(semi(z)\ge semi(x)\)那么\(idom(x)=semi(x)\)

​ 证明:如果最小的\(semi(z)\)\(\ge semi(x)\),那么也就是说没有\(semi(x)\)的祖先能绕过\(semi(x)\)到达\(P\)中的点,那么\(semi(x)\)支配\(x\),故\(semi(x)\cdot\rightarrow idom(x)\),根据引理\(2\)\(idom(x)\cdot \rightarrow semi(x)\),于是\(idom(x)=semi(x)\)

  • 定理\(2\):如果\(semi(z)<semi(x)\)那么\(idom(x)=idom(z)\)

    感性理解:条件即\(semi(z)\cdot\rightarrow semi(x)\cdot\rightarrow z\cdot \rightarrow x\),根据引理\(3\)\(z\cdot \rightarrow idom(x)\)\(idom(x)\cdot \rightarrow idom(z)\)

    • 如果\(z\cdot\rightarrow idom(x)\),由引理\(2\)\(idom(x)\cdot \rightarrow semi(x)\),于是\(idom(x)\cdot \rightarrow z\),矛盾
    • 否则,取\(s\)\(x\)的路径上最后一个\(<idom(z)\)\(w\),取\(w\)最小的一个后继\(y\)使\(idom(z)\cdot\rightarrow y\cdot \rightarrow x\)。显然\(w\)\(y\)的路径上不能有\(v\)使\(idom(z)\cdot\rightarrow v+\rightarrow y\)(否则\(v\)\(y\)的位置),那么这条路径只经过\(>y\)的点,于是\(w\)满足\(semi(y)\)的条件,\(w\ge semi(y)\),于是\(semi(y)\le w<idom(z)\le semi(z)\le semi(x)<z\le x\)
    • 注意到\(y\)一定不会在\(semi(x)\)\(x\)的路径上,否则就违反了\(semi(z)\)最小这一前提
    • 同时\(y\)也不再\(idom(z)\)\(semi(x)\)的路径上,否则我们就能通过到\(semi(y)\)再到\(y\)再到\(z\)绕开\(idom(z)\)到达了\(z\)
    • 再加上\(idom(z)\cdot\rightarrow y\),于是只剩下一种选择:\(idom(z)=y\),因为\(y\)支配\(x\)于是\(idom(z)\)支配\(x\),进而得到\(idom(x)=idom(z)\)

那么有了这\(2\)个定理,我们就能够推出\(idom(x)\)了,具体实现看代码注释:

inline int find(int x){//带权并查集,mi[x]是x到根路径上满足dfn[semi[k]]最小的k 
	if(f[x]==x) return x;
	int t=f[x];f[x]=find(f[x]);
	if(dfn[semi[mi[t]]]<dfn[semi[mi[x]]]) mi[x]=mi[t];
	return f[x];
}//路径压缩后用路径上的semi更新当前点维护的mi 
inline void findidom(){
	for(int i=1;i<=n;++i) mi[i]=semi[i]=f[i]=i;
	for(int i=n;i>=2;--i){//按dfs序倒序枚举 
		int u=pos[i],sem=n;
		for(int j=rg.first[u];j;j=rg.e[j].nxt){
			int v=rg.e[j].v;
			if(dfn[v]<dfn[u]) sem=min(sem,dfn[v]);//dfn[v]<dfn[u]直接更新semi
			else find(v),sem=min(sem,dfn[semi[mi[v]]]);//否则,用v祖先中semi最小的一个更新semu 
		}
		semi[u]=pos[sem];f[u]=fa[u];//找到semi[u],将它并入并查集中 
		ng.add(semi[u],u); 
		
		u=pos[i-1];//此时,所有semi可能是u的点一定已经全部找出(因为dfs序大于dfn[u]的都已经考虑过了) 
		for(int j=ng.first[u];j;j=ng.e[j].nxt){
			int v=ng.e[j].v;
			find(v);//u一定是v的祖先,此时取并查集上维护的min就一定能取到(u,v)当前路径上dfn[semi[x]]最小的x 
			if(semi[mi[v]]==u) idom[v]=u;
			//当最小的semi=u时,说明没有点能绕过u来到v,那么u就是v的支配点 
			else idom[v]=mi[v];
			//否则idom[v]就应该是idom[mi[v]] 
			//但mi[v]的支配点还没有找到,也就无法更新,先记录下来,等全部递归完后,再用idom[v]=idom[idom[v]]更新 
		}
	}
	for(int i=2;i<=n;++i){//正序遍历,保证遍历到u时mi[u]的支配点一定已经找到 
		int u=pos[i];
		if(idom[u]!=semi[u]) idom[u]=idom[idom[u]];//idom[u]!=semi[u]说明是上面所说的第二条注释 
		tr.add(idom[u],u);
	}
}

至此,我们终于完成了构建支配树的全过程,或许理解比较难,但它的代码还是比较短的

在这里给出洛谷模板题的代码:

#include<bits/stdc++.h>
using namespace std;
inline int read(){
	int x=0,f=1;
	char ch=getchar();
	while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
	while(isdigit(ch)){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
	return x*f;
}
const int N=3e5+10;
int n,m,tot;
int dfn[N],pos[N],mi[N],fa[N],f[N];
int semi[N],idom[N];
struct node{
	int v,nxt;
};
struct graph{
	int first[N],cnt;
	node e[N];
	inline void add(int u,int v){e[++cnt].v=v;e[cnt].nxt=first[u];first[u]=cnt;}
}g,rg,ng,tr;//g:原图 rg:反图 ng:仅保留dfs树与(semi[x],x)的图 tr:支配树
inline void dfs(int u){
	dfn[u]=++tot;pos[tot]=u;
	for(int i=g.first[u];i;i=g.e[i].nxt){
		int v=g.e[i].v;
		if(!dfn[v]) fa[v]=u,dfs(v);
	}
}
inline int find(int x){//带权并查集,mi[x]是x到根路径上满足dfn[semi[k]]最小的k 
	if(f[x]==x) return x;
	int t=f[x];f[x]=find(f[x]);
	if(dfn[semi[mi[t]]]<dfn[semi[mi[x]]]) mi[x]=mi[t];
	return f[x];
}//路径压缩后用路径上的semi更新当前点维护的mi 
inline void findidom(){
	for(int i=1;i<=n;++i) mi[i]=semi[i]=f[i]=i;
	for(int i=n;i>=2;--i){//按dfs序倒序枚举 
		int u=pos[i],sem=n;
		for(int j=rg.first[u];j;j=rg.e[j].nxt){
			int v=rg.e[j].v;
			if(dfn[v]<dfn[u]) sem=min(sem,dfn[v]);//dfn[v]<dfn[u]直接更新semi
			else find(v),sem=min(sem,dfn[semi[mi[v]]]);//否则,用v祖先中semi最小的一个更新semu 
		}
		semi[u]=pos[sem];f[u]=fa[u];//找到semi[u],将它并入并查集中 
		ng.add(semi[u],u); 
		
		u=pos[i-1];//此时,所有semi可能是u的点一定已经全部找出(因为dfs序大于dfn[u]的都已经考虑过了) 
		for(int j=ng.first[u];j;j=ng.e[j].nxt){
			int v=ng.e[j].v;
			find(v);//u一定是v的祖先,此时取并查集上维护的min就一定能取到(u,v)当前路径上dfn[semi[x]]最小的x 
			if(semi[mi[v]]==u) idom[v]=u;
			//当最小的semi=u时,说明没有点能绕过u来到v,那么u就是v的支配点 
			else idom[v]=mi[v];
			//否则idom[v]就应该是idom[mi[v]] 
			//但mi[v]的支配点还没有找到,也就无法更新,先记录下来,等全部递归完后,再用idom[v]=idom[idom[v]]更新 
		}
	}
	for(int i=2;i<=n;++i){//正序遍历,保证遍历到u时mi[u]的支配点一定已经找到 
		int u=pos[i];
		if(idom[u]!=semi[u]) idom[u]=idom[idom[u]];//idom[u]!=semi[u]说明是上面所说的第二条注释 
		tr.add(idom[u],u);
	}
}
int siz[N];
inline void dfs_tr(int u){//遍历支配树求siz 
	siz[u]=1;
	for(int i=tr.first[u];i;i=tr.e[i].nxt){
		int v=tr.e[i].v;
		dfs_tr(v);siz[u]+=siz[v];
	}
}
int main(){
	n=read();m=read();
	for(int i=1,u,v;i<=m;++i){
		u=read();v=read();
		g.add(u,v);rg.add(v,u);
	}
	dfs(1);//建出dfs树 
	findidom(); //建出支配树
	dfs_tr(1);
	for(int i=1;i<=n;++i) printf("%d ",siz[i]);
	return 0; 
}
posted @ 2021-01-17 16:09  cjTQX  阅读(189)  评论(0编辑  收藏  举报