[学习笔记]支配树
〇、前言 ¶
这个东西......我真的是吐了,要是在省选之前把它完善了......可是没多的时间了。
这个东西赶脚见到得很少,但是遇到的时候就很有用了,所以还是写一下。实际上很多部分都是直接宅的,但是加入了一些自己的理解。
壹、这是什么 ¶
一个有向图 \(G=\lang V,E\rang\),给定一个起点 \(s\),假设 \(s\) 到能够访问到其他所有顶点。若去掉某点 \(i\) 后,从 \(s\) 无法访问到 \(j\),则称 \(i\) 为 \(j\) 的支配点。显然支配关系满足传递性,若 \(i\) 支配 \(j\),\(j\) 支配 \(k\),则 \(i\) 支配 \(k\).
什么是支配树呢?
以 \(s\) 为根节点建一个有向树,使得对于任意节点 \(i\),其树上的祖先均为 \(i\) 的支配点,这样的树称为支配树。任何有向图都存在支配树,前提是从 \(s\) 能够访问到所有顶点。
注意:支配树并不一定是 \(G\) 的树形图。因为有些边不是 \(E\) 中的边。
贰、一些定义与约定 ¶
先给出一些定义:
- \(\rm dfs\) 树:从点 \(s\) 出发,对 \(G\) 进行一次 \(\tt dfs\),则可以得到一棵 \(\tt dfs\) 树(下文称其为 \(\rm dfst\)),在 \(\tt dfs\) 时,要根据访问的先后顺序对顶点编号,即给每个顶点打上时间戳 \(\tt dfn\). \(\rm dfst\) 中的边称为树边,不在 \(\rm dfst\) 树中的边称为非树边。非树边又分为前向边、返祖边、横叉边。返祖边即从 \(\rm dfst\) 中一个点出发,连接到它在树上的祖先的边,前向边即从树上的某个点直接连接到它的后代,跨越了被连接点的父亲,横叉边即从一棵子树连接到另一棵子树上去。
- 直接支配点 \(idom\):点 \(i\) 的支配点中 \(\tt dfn\) 值最大的点即为点 \(i\) 的直接支配点,换句话说,点 \(i\) 在支配树中的父亲即它的直接支配点。直接支配点一般用 \(idom\) 表示;
- 半支配点 \(sdom\)(\(\rm semi-dominator\)):顶点 \(u\) 的半支配点定义为 \(sdom_u=\min\{{\tt dfn}_v|\exist (v_0,v_1,v_2,...,v_k),v_0=v,v_k=u,\forall 1\le i\le k-1,{\tt dfn}_v>{\tt dfn}_u\}\),说人话就是所有满足从 \(v\) 到 \(u\) 存在一条所有点的 \(\tt dfn\) 大于 \(u\) 的 \(\tt dfn\) 的路径,除了开头 \(v\) 和结尾 \(u\) 的 \(v\) 中 \(\tt dfn\) 最小的那个。再直观一点就是想象在某两条 \(\tt dfst\) 的子链 \(l,m\) 上,他们的分叉为 \(v\) 且 \(u\in l\) 且 \(l\) 的 \(\tt dfn\) 比 \(m\) 小,如果从 \(m\) 有一条横叉边连向 \(u\),那么就可以说 \(v\) 是一个待选的半支配点,从所有待选半支配点中找出那个 \(\tt dfn\) 最小的,这个点就是 \(u\) 的半支配点,较特别的,若 \(v\rightarrow u\),那么 \(v\) 也是一个待选半支配点;
然后是一些符号的约定,其实就是四个箭头:
- \(u\rightarrow v\),表示 \(u\) 存在一条边由 \(u\) 到 \(v\);
- \(u\overset{+}{\rightarrow} v\),表示 \(u\) 存在一条由树边组成的路径到 \(v\),且 \(u\neq v\);
- \(u\overset{.}{\rightarrow} v\),表示 \(u\) 存在一条由树边组成的路径到 \(v\),允许 \(u=v\);
- \(u\leadsto v\),表示 \(u\) 存在一条路径到 \(v\);
叁、定理、引理和推论 ¶
遵循 \(5,3,1\) 的数字规律,一个一个给出。
1.1.§ 五大引理 §
1.1.1.直接支配存在引理
对于 \(G=\lang V,E\rang\),除了 \(s\) 以外的所有点都存在 \(idom\).
每个点有时间戳,每个点至少有一个支配点。所以一定有 \(\tt dfn\) 最小的支配点。
我有一个 apple,我有一个 pen,所以我有 apple pen.
1.1.2.直接支配祖先引理
\(\forall u\neq s\) 有 \(idom_u\overset{+}{\rightarrow}u\).
如果 \(idom_u\) 不是 \(u\) 的祖先,那 \(s\) 不是可以直接沿着树边到 \(u\) 而不用经过 \(idom_u\) 了吗......
1.1.3.半支配祖先引理
对于 \(G=\lang V,E\rang\),\(\forall u\neq s\) 有 \(sdom_u\overset{+}{\rightarrow}u\).
根据定义,\(sdom_u\) 有一条由非树边组成的路径到达 \(u\),且路径上的点 \(i\) 满足 \({\tt dfn}_i>{\tt dfn}_u\),又 \({\tt dfn}_{sdom_u}<{\tt dfn}_u\),且在所有满足这个条件的点中, \({\tt dfn}_{sdom_u}\) 最小。
考虑取 \(sdom_u\) 和 \(u\) 的 \(\tt LCA\),记为 \(p\),那么 \(t\) 一定是路径上一点,且 \({\tt dfn}_t\le {\tt dfn}_{sdom_u}<{\tt dfn}_u\),而路径中间的顶点 \(i\),有 \({\tt dfn}_i<{\tt dfn}_u\),所以 \(t\) 只能是 \(sdom_u\).
1.1.4.直接支配最高引理
\(\forall u\neq s\),有 \(idom_u\overset{.}{\rightarrow}sdom_u\).
根据 \(1.1.2\) 与 \(1.1.3\) 可知 \(idom_u\) 和 \(sdom_u\) 位于 \(s\) 到某个 \(\tt dfst\) 某个叶节点的路径上。
所以,只要排除 \(sdom_u\overset{.}{\rightarrow}idom_u\) 即可。
考虑反证法,假设 \(sdom_u\overset{.}{\rightarrow}idom_u\),由 \(sdom_u\) 的定义,不难发现 \(idom_u\) 此时不再支配 \(u\),与定义矛盾,所以原命题得证。
1.1.5.直接支配嵌套引理
\(\forall u\overset{.}{\rightarrow} v\),有 \(u\overset{.}{\rightarrow} idom_v\or idom_v\overset{.}{\rightarrow} idom_u\).
肯定地,\(u,v,idom_u,idom_v\) 一定在 \(\tt dfst\) 从 \(s\) 到某个叶子的路径上。
分情况讨论:
- 当 \({\tt dfn}_u\le {\tt dfn}_{idom_v}\),则有 \(u\overset{.}{\rightarrow}{\tt dfn}_{idom_v} \overset{.}{\rightarrow}v\);
- 当 \({\tt dfn}_u>{\tt dfn}_{idom_v}\),则有 \({\tt dfn}_{idom_v}\overset{.}{\rightarrow} u\),此时考察 \(idom_u\) 与 \(idom_v\) 的关系:如果 \(idom_u\overset{.}{\rightarrow}idom_v\),则去掉 \(idom_v\) 还是可以从 \(idom_u\) 到 \(u\) 再到 \(v\),与 \(idom_v\) 定义矛盾,所以只能是 \(idom_v\overset{.}{\rightarrow}idom_u\);
1.2.§ 三大定理 §
1.2.1.封闭定理
\(\forall u\neq s\),若 \(\forall v\;{\rm s.t.}\;sdom_u\overset{+}{\rightarrow} v\overset{.}{\rightarrow} u\) 满足 \({\tt dfn}_{sdom_v}>{\tt dfn}_{sdom_u}\),则 \(idom_u=sdom_u\).
感性证明,可以理解为所有在 \(sdom_u\) 管辖范围内的所有点都无法连出去而被封闭在里面了,那么 \(u\) 就一定被 \(sdom_u\) 支配。
1.2.2.继承定理
\(\forall u\neq s\),若 \(\exist v\;{\rm s.t.}\;sdom_u\overset{+}{\rightarrow} v\overset{.}{\rightarrow} u\and{\tt dfn}_{sdom_v}<{\tt dfn}_{sdom_u}\),设 \(p\) 为所有 \(v\) 中 \({\tt dfn}_{sdom_v}\) 最小的一个,则 \(idom_p=idom_u\).
感性证明一下,可以理解为如果有 \(v\) 从 \(sdom_u\) 连出去了,那么 \(u\) 就摆脱了 \(sdom_u\) 的束缚,但由于他是通过 \(v\) 摆脱束缚的,所以束缚 \(v\) 的点必定束缚 \(u\),因而可以直接继承直接支配点。
1.2.3.半支配定理
\(\forall u\neq s,sdom_u=\min\{v|(v,u)\in E,{\tt dfn}_v<{\tt dfn}_u\cup sdom_p|{\tt dfn}_p>{\tt dfn}_u\and \exist (q,w)\in E,p\overset{.}{\rightarrow}q\}\)
对于第 \(1\) 类,即 \(v|(v,u)\in E,{\tt dfn}_v<{\tt dfn}_u\),这是 \(sdom\) 的特殊情况。
对于第 \(2\) 类,即 \(sdom_p|{\tt dfn}_p>{\tt dfn}_u,\exist (q,w)\in E,p\overset{.}{\rightarrow}q\),其实就是考虑横叉边的情况。
1.3.§ 第一推论——直接支配推论 §
\(\forall u\neq s\),令 \(v\) 为所有满足 \(sdom_u\overset{+}{\rightarrow} v\overset{.}{\rightarrow} u\) 中 \({\tt dfn}_{sdom_v}\)最小的点,那么
其实就是将前两个定理 ruá 在了一起,其实在与上面取等的时候,就是 \(v\) 取到 \(u\) 或者 \(v,u\) 有共同半支配点的时候,即封闭定理的情况。
由这个推论不难看出,对于一个新图 \(G'=\lang V,E'\rang\),其中 \(E'=\{(sdom_i,i)|i\neq s\}\),那么 \(G\) 和 \(G'\) 的支配关系是一样的,因为第二种取等情况,可以一直递归至第一种情况,并且一定有 \(G'\) 是一个 \(\tt DAG\).
肆、建树 ¶
大致分为几种不同的图:
- 树,自己就是支配树;
- \(\tt DAG\),则对原图进行拓扑排序,依次确定 \(idom\),设当前点为 \(i\),若 \(j\rightarrow i\),则所有 \(j\) 的 \(\tt LCA\) 就是 \(idom_i\);
- 有向图,由直接支配推论,我们可以将 \(G\) 转化为 \(G'\),然后在这个 \(\tt DAG\) 上使用拓扑即可建出支配树。或者使用另外一种方法,将所有点的 \(sdom\) 找出来之后,使用推论寻找 \(idom\),这样可以避免寻找 \(\tt LCA\),将 \(\tt LCA\) 的 \(\log\) 变成并查集的一般 \(\mathcal O(n\alpha)\),很少会有变态把并查集卡成 \(\mathcal O(\log)\) 的吧?
最复杂情况即一般有向图,时间复杂度 \(\mathcal O(n\alpha)\) 或 \(\mathcal O(n\log n)\).
伍、参考代码 ¶
使用的 \(\mathcal O(n\log n)\) 的算法,毕竟太懒了......
#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;
}
陆、容易锅的地方 ¶
这些锅可能出现在我的某次考试中,或者原题检测、或者一些其他的时候......
谁知道呢,反正这些地方容易出锅就对了。
- 由于 \(\tt tail[]\) 是以 \(-1\) 结尾的,所以有些地方潜伏的 \(\color{violet}{\text{RE}}\);
- 多组数据的时候,不止清空 \(\tt dfn[]\),还要记得 \(\tt refl[]\);
- 进行 \(\tt dfs()\) 的时候,记得加上 \(\tt !dfn[v]\);
- 注意支配树的根是 \(0\),所以支配树的 \(\tt dfn\) 上限不只是 \(n\),而是 \(n+1\);