[学习笔记]支配树

〇、前言 ¶

这个东西......我真的是吐了,要是在省选之前把它完善了......可是没多的时间了。

这个东西赶脚见到得很少,但是遇到的时候就很有用了,所以还是写一下。实际上很多部分都是直接宅的,但是加入了一些自己的理解。

壹、这是什么 ¶

一个有向图 G=V,E,给定一个起点 s,假设 s 到能够访问到其他所有顶点。若去掉某点 i 后,从 s 无法访问到 j,则称 ij 的支配点。显然支配关系满足传递性,若 i 支配 jj 支配 k,则 i 支配 k.

什么是支配树呢?

s 为根节点建一个有向树,使得对于任意节点 i,其树上的祖先均为 i 的支配点,这样的树称为支配树。任何有向图都存在支配树,前提是从 s 能够访问到所有顶点。

注意:支配树并不一定是 G 的树形图。因为有些边不是 E 中的边。

贰、一些定义与约定 ¶

先给出一些定义:

  1. dfs 树:从点 s 出发,对 G 进行一次 dfs,则可以得到一棵 dfs 树(下文称其为 dfst),在 dfs 时,要根据访问的先后顺序对顶点编号,即给每个顶点打上时间戳 dfn. dfst 中的边称为树边,不在 dfst 树中的边称为非树边。非树边又分为前向边、返祖边、横叉边。返祖边即从 dfst 中一个点出发,连接到它在树上的祖先的边,前向边即从树上的某个点直接连接到它的后代,跨越了被连接点的父亲,横叉边即从一棵子树连接到另一棵子树上去。
  2. 直接支配点 idom:点 i 的支配点中 dfn 值最大的点即为点 i 的直接支配点,换句话说,点 i 在支配树中的父亲即它的直接支配点。直接支配点一般用 idom 表示;
  3. 半支配点 sdomsemidominator):顶点 u 的半支配点定义为 sdomu=min{dfnv|(v0,v1,v2,...,vk),v0=v,vk=u,1ik1,dfnv>dfnu},说人话就是所有满足从 vu 存在一条所有点的 dfn 大于 udfn 的路径,除了开头 v 和结尾 uvdfn 最小的那个。再直观一点就是想象在某两条 dfst 的子链 l,m 上,他们的分叉为 vulldfnm 小,如果从 m 有一条横叉边连向 u,那么就可以说 v 是一个待选的半支配点,从所有待选半支配点中找出那个 dfn 最小的,这个点就是 u 的半支配点,较特别的,若 vu,那么 v 也是一个待选半支配点;

然后是一些符号的约定,其实就是四个箭头:

  • uv,表示 u 存在一条边由 uv
  • u+v,表示 u 存在一条由树边组成的路径到 v,且 uv
  • u.v,表示 u 存在一条由树边组成的路径到 v,允许 u=v
  • uv,表示 u 存在一条路径到 v

叁、定理、引理和推论 ¶

遵循 5,3,1 的数字规律,一个一个给出。

1.1.§ 五大引理 §

1.1.1.直接支配存在引理

对于 G=V,E,除了 s 以外的所有点都存在 idom.

每个点有时间戳,每个点至少有一个支配点。所以一定有 dfn 最小的支配点。

我有一个 apple,我有一个 pen,所以我有 apple pen.

1.1.2.直接支配祖先引理

usidomu+u.

如果 idomu 不是 u 的祖先,那 s 不是可以直接沿着树边到 u 而不用经过 idomu 了吗......

1.1.3.半支配祖先引理

对于 G=V,Eussdomu+u.

根据定义,sdomu 有一条由非树边组成的路径到达 u,且路径上的点 i 满足 dfni>dfnu,又 dfnsdomu<dfnu,且在所有满足这个条件的点中, dfnsdomu 最小。

考虑取 sdomuuLCA,记为 p,那么 t 一定是路径上一点,且 dfntdfnsdomu<dfnu,而路径中间的顶点 i,有 dfni<dfnu,所以 t 只能是 sdomu.

1.1.4.直接支配最高引理

us,有 idomu.sdomu.

根据 1.1.21.1.3 可知 idomusdomu 位于 s 到某个 dfst 某个叶节点的路径上。

所以,只要排除 sdomu.idomu 即可。

考虑反证法,假设 sdomu.idomu,由 sdomu 的定义,不难发现 idomu 此时不再支配 u,与定义矛盾,所以原命题得证。

1.1.5.直接支配嵌套引理

u.v,有 u.idomvidomv.idomu.

肯定地,u,v,idomu,idomv 一定在 dfsts 到某个叶子的路径上。

分情况讨论:

  • dfnudfnidomv,则有 u.dfnidomv.v
  • dfnu>dfnidomv,则有 dfnidomv.u,此时考察 idomuidomv 的关系:如果 idomu.idomv,则去掉 idomv 还是可以从 idomuu 再到 v,与 idomv 定义矛盾,所以只能是 idomv.idomu

1.2.§ 三大定理 §

1.2.1.封闭定理

us,若 vs.t.sdomu+v.u 满足 dfnsdomv>dfnsdomu,则 idomu=sdomu.

感性证明,可以理解为所有在 sdomu 管辖范围内的所有点都无法连出去而被封闭在里面了,那么 u 就一定被 sdomu 支配。

1.2.2.继承定理

us,若 vs.t.sdomu+v.udfnsdomv<dfnsdomu,设 p 为所有 vdfnsdomv 最小的一个,则 idomp=idomu.

感性证明一下,可以理解为如果有 vsdomu 连出去了,那么 u 就摆脱了 sdomu 的束缚,但由于他是通过 v 摆脱束缚的,所以束缚 v 的点必定束缚 u,因而可以直接继承直接支配点。

1.2.3.半支配定理

us,sdomu=min{v|(v,u)E,dfnv<dfnusdomp|dfnp>dfnu(q,w)E,p.q}

对于第 1 类,即 v|(v,u)E,dfnv<dfnu,这是 sdom 的特殊情况。

对于第 2 类,即 sdomp|dfnp>dfnu,(q,w)E,p.q,其实就是考虑横叉边的情况。

1.3.§ 第一推论——直接支配推论 §

us,令 v 为所有满足 sdomu+v.udfnsdomv最小的点,那么

idomu={sdomvsdomu=sdomvidomvdfnsdomv<dfnsdomu

其实就是将前两个定理 ruá 在了一起,其实在与上面取等的时候,就是 v 取到 u 或者 v,u 有共同半支配点的时候,即封闭定理的情况。

由这个推论不难看出,对于一个新图 G=V,E,其中 E={(sdomi,i)|is},那么 GG 的支配关系是一样的,因为第二种取等情况,可以一直递归至第一种情况,并且一定有 G 是一个 DAG.

肆、建树 ¶

大致分为几种不同的图:

  • 树,自己就是支配树;
  • DAG,则对原图进行拓扑排序,依次确定 idom,设当前点为 i,若 ji,则所有 jLCA 就是 idomi
  • 有向图,由直接支配推论,我们可以将 G 转化为 G,然后在这个 DAG 上使用拓扑即可建出支配树。或者使用另外一种方法,将所有点的 sdom 找出来之后,使用推论寻找 idom,这样可以避免寻找 LCA,将 LCAlog 变成并查集的一般 O(nα),很少会有变态把并查集卡成 O(log) 的吧?

最复杂情况即一般有向图,时间复杂度 O(nα)O(nlogn).

伍、参考代码 ¶

使用的 O(nlogn) 的算法,毕竟太懒了......

#include<cstdio>
#include<queue>
#include<vector>
#include<cstring>
#include<algorithm>
using namespace std;

#define Endl putchar('\n')
typedef long long ll;
template<class T>inline T readin(T x){
	x=0; int f=0; char c;
	while((c=getchar())<'0' || '9'<c) if(c=='-') f=1;
	for(x=(c^48); '0'<=(c=getchar()) && c<='9'; x=(x<<1)+(x<<3)+(c^48));
	return f? -x: x;
}

const int maxn=2e5;
const int maxm=3e5;
const int logn=18;

int n, m;

struct graph{
	struct edge{
		int to, nxt;
		edge(){}
		edge(int T, int N): to(T), nxt(N){}
	}e[maxn*2+5];
	int tail[maxn+5], ecnt;
	inline void add_edge(int u, int v){
		e[ecnt]=edge(v, tail[u]); tail[u]=ecnt++;
	}
	graph(){
		memset(tail, -1,  sizeof tail);
		ecnt=0;
	}
}G, rG, dag, rdag, domi_tre;
#define foredge(u, G) for(int i=G.tail[u], v=G.e[i].to; ~i; i=G.e[i].nxt, v=G.e[i].to)

inline void input(){
	n=readin(1), m=readin(1);
	int u, v;
	for(int i=1; i<=m; ++i){
		u=readin(1), v=readin(1);
		G.add_edge(u, v);
		rG.add_edge(v, u);
	}
}

// get the dfst
int dfn[maxn+5], refl[maxn+5], fa[maxn+5], timer;
void dfs(int u){
	refl[dfn[u]=++timer]=u;
	foredge(u, G) if(!dfn[v]){
		dfs(v); fa[v]=u;
		dag.add_edge(u, v); // special sdom
	}
}

// dsu
int pre[maxn+5], minn[maxn+5], sdom[maxn+5];
inline void initial(){
	for(int i=1; i<=n; ++i)
		pre[i]=minn[i]=sdom[i]=i;
}
int findrt(int u){
	if(u==pre[u]) return u;
	int ret=findrt(pre[u]);
	if(dfn[sdom[minn[u]]]>dfn[sdom[minn[pre[u]]]])
		minn[u]=minn[pre[u]];
	return pre[u]=ret;
}

inline void tarjan(){
	initial(); // pay attention
	// enumerate dfn from big to small
	for(int j=n; j>1; --j){
		int u=refl[j], res=j;
		if(!u) continue; // s connot reach u
		foredge(u, rG){
			if(!dfn[v]) continue; // s connot reach v
			// an edge which connect u and the node outside
			if(dfn[v]<dfn[u]) res=min(res, dfn[v]);
			else findrt(v), res=min(res, dfn[sdom[minn[v]]]);
		}
		sdom[u]=refl[res];
		pre[u]=fa[u];
		dag.add_edge(sdom[u], u);
	}
}

int dep[maxn+5], tp[maxn+5][logn+5];
inline int getlca(int u, int v){
	if(dep[u]<dep[v]) swap(u, v);
	for(int j=logn; j>=0; --j) if(dep[tp[u][j]]>=dep[v])
		u=tp[u][j];
	if(u==v) return u;
	for(int j=logn; j>=0; --j) if(tp[u][j]!=tp[v][j])
		u=tp[u][j], v=tp[v][j];
	return tp[u][0];
}
inline void insert_tre(int u){
	int anc=rdag.e[rdag.tail[u]].to;
	foredge(u, rdag) anc=getlca(anc, v);
	dep[u]=dep[anc]+1;
	tp[u][0]=anc;
	domi_tre.add_edge(anc, u);
	for(int j=1; j<=logn; ++j)
		tp[u][j]=tp[tp[u][j-1]][j-1];
}
int in[maxn+5];
queue<int>Q;
inline void topu(){
	for(int u=1; u<=n; ++u){
		foredge(u, dag){
			++in[v];
			rdag.add_edge(v, u);
		}
	}
	for(int i=1; i<=n; ++i) if(!in[i]){
		dag.add_edge(0, i), rdag.add_edge(i, 0);
		++in[i];
	}
	Q.push(0);
	while(!Q.empty()){
		int u=Q.front(); Q.pop();
		foredge(u, dag){
			if(!--in[v]){
				Q.push(v);
				insert_tre(v);
			}
		}
	}
}

int siz[maxn+5];
void dfstre(int u){
	siz[u]=1;
	foredge(u, domi_tre){
		dfstre(v);
		siz[u]+=siz[v];
	}
}

signed main(){
	input();
	dfs(1);
	tarjan();
	topu();
	dfstre(0);
	for(int i=1; i<=n; ++i)
		printf("%d ", siz[i]);
	Endl;
	return 0;
}

陆、容易锅的地方 ¶

这些锅可能出现在我的某次考试中,或者原题检测、或者一些其他的时候......

谁知道呢,反正这些地方容易出锅就对了。

  • 由于 tail[] 是以 1 结尾的,所以有些地方潜伏的 RE
  • 多组数据的时候,不止清空 dfn[],还要记得 refl[]
  • 进行 dfs() 的时候,记得加上 !dfn[v]
  • 注意支配树的根是 0,所以支配树的 dfn 上限不只是 n,而是 n+1
posted @   Arextre  阅读(951)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示