• 支配树学习笔记


    一、引入

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

    支配树是个什么东西呢?

    首先我们定义支配点:在一个有向图上,指定一个起点(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
    ",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)

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

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

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

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

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

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

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

    半支配点

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

    重要引理:

    • 引理(1)(semi(x)+ ightarrow x)

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

    • 引理(2)(idom(x)cdot ightarrow semi(x))

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

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

      证明:如果不成立,则(idom(x)+ ightarrow idom(y)+ ightarrow x+ ightarrow 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)为满足(zcdot ightarrow 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 ot= 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 ightarrow idom(x)),根据引理(2)(idom(x)cdot ightarrow semi(x)),于是(idom(x)=semi(x))

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

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

      • 如果(zcdot ightarrow idom(x)),由引理(2)(idom(x)cdot ightarrow semi(x)),于是(idom(x)cdot ightarrow z),矛盾
      • 否则,取(s)(x)的路径上最后一个(<idom(z))(w),取(w)最小的一个后继(y)使(idom(z)cdot ightarrow ycdot ightarrow x)。显然(w)(y)的路径上不能有(v)使(idom(z)cdot ightarrow v+ ightarrow y)(否则(v)(y)的位置),那么这条路径只经过(>y)的点,于是(w)满足(semi(y))的条件,(wge semi(y)),于是(semi(y)le w<idom(z)le semi(z)le semi(x)<zle x)
      • 注意到(y)一定不会在(semi(x))(x)的路径上,否则就违反了(semi(z))最小这一前提
      • 同时(y)也不再(idom(z))(semi(x))的路径上,否则我们就能通过到(semi(y))再到(y)再到(z)绕开(idom(z))到达了(z)
      • 再加上(idom(z)cdot ightarrow 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; 
    }
    
  • 相关阅读:
    Systemverilog for design 笔记(三)
    SystemVerilog for design 笔记(二)
    Systemverilog for design 笔记(一)
    假如m是奇数,且m>=3,证明m(m² -1)能被8整除
    SharpSvn操作 -- 获取Commit节点列表
    GetRelativePath获取相对路径
    Dictionary(支持 XML 序列化),注意C#中原生的Dictionary类是无法进行Xml序列化的
    Winform中Checkbox与其他集合列表类型之间进行关联
    Image(支持 XML 序列化),注意C#中原生的Image类是无法进行Xml序列化的
    修复使用<code>XmlDocument</code>加载含有DOCTYPE的Xml时,加载后增加“[]”字符的错误
  • 原文地址:https://www.cnblogs.com/tqxboomzero/p/14289261.html
Copyright © 2020-2023  润新知