[学习笔记]支配树
〇、前言 ¶
这个东西......我真的是吐了,要是在省选之前把它完善了......可是没多的时间了。
这个东西赶脚见到得很少,但是遇到的时候就很有用了,所以还是写一下。实际上很多部分都是直接宅的,但是加入了一些自己的理解。
壹、这是什么 ¶
一个有向图 ,给定一个起点 ,假设 到能够访问到其他所有顶点。若去掉某点 后,从 无法访问到 ,则称 为 的支配点。显然支配关系满足传递性,若 支配 , 支配 ,则 支配 .
什么是支配树呢?
以 为根节点建一个有向树,使得对于任意节点 ,其树上的祖先均为 的支配点,这样的树称为支配树。任何有向图都存在支配树,前提是从 能够访问到所有顶点。
注意:支配树并不一定是 的树形图。因为有些边不是 中的边。
贰、一些定义与约定 ¶
先给出一些定义:
- 树:从点 出发,对 进行一次 ,则可以得到一棵 树(下文称其为 ),在 时,要根据访问的先后顺序对顶点编号,即给每个顶点打上时间戳 . 中的边称为树边,不在 树中的边称为非树边。非树边又分为前向边、返祖边、横叉边。返祖边即从 中一个点出发,连接到它在树上的祖先的边,前向边即从树上的某个点直接连接到它的后代,跨越了被连接点的父亲,横叉边即从一棵子树连接到另一棵子树上去。
- 直接支配点 :点 的支配点中 值最大的点即为点 的直接支配点,换句话说,点 在支配树中的父亲即它的直接支配点。直接支配点一般用 表示;
- 半支配点 ():顶点 的半支配点定义为 ,说人话就是所有满足从 到 存在一条所有点的 大于 的 的路径,除了开头 和结尾 的 中 最小的那个。再直观一点就是想象在某两条 的子链 上,他们的分叉为 且 且 的 比 小,如果从 有一条横叉边连向 ,那么就可以说 是一个待选的半支配点,从所有待选半支配点中找出那个 最小的,这个点就是 的半支配点,较特别的,若 ,那么 也是一个待选半支配点;
然后是一些符号的约定,其实就是四个箭头:
- ,表示 存在一条边由 到 ;
- ,表示 存在一条由树边组成的路径到 ,且 ;
- ,表示 存在一条由树边组成的路径到 ,允许 ;
- ,表示 存在一条路径到 ;
叁、定理、引理和推论 ¶
遵循 的数字规律,一个一个给出。
1.1.§ 五大引理 §
1.1.1.直接支配存在引理
对于 ,除了 以外的所有点都存在 .
每个点有时间戳,每个点至少有一个支配点。所以一定有 最小的支配点。
我有一个 apple,我有一个 pen,所以我有 apple pen.
1.1.2.直接支配祖先引理
有 .
如果 不是 的祖先,那 不是可以直接沿着树边到 而不用经过 了吗......
1.1.3.半支配祖先引理
对于 , 有 .
根据定义, 有一条由非树边组成的路径到达 ,且路径上的点 满足 ,又 ,且在所有满足这个条件的点中, 最小。
考虑取 和 的 ,记为 ,那么 一定是路径上一点,且 ,而路径中间的顶点 ,有 ,所以 只能是 .
1.1.4.直接支配最高引理
,有 .
根据 与 可知 和 位于 到某个 某个叶节点的路径上。
所以,只要排除 即可。
考虑反证法,假设 ,由 的定义,不难发现 此时不再支配 ,与定义矛盾,所以原命题得证。
1.1.5.直接支配嵌套引理
,有 .
肯定地, 一定在 从 到某个叶子的路径上。
分情况讨论:
- 当 ,则有 ;
- 当 ,则有 ,此时考察 与 的关系:如果 ,则去掉 还是可以从 到 再到 ,与 定义矛盾,所以只能是 ;
1.2.§ 三大定理 §
1.2.1.封闭定理
,若 满足 ,则 .
感性证明,可以理解为所有在 管辖范围内的所有点都无法连出去而被封闭在里面了,那么 就一定被 支配。
1.2.2.继承定理
,若 ,设 为所有 中 最小的一个,则 .
感性证明一下,可以理解为如果有 从 连出去了,那么 就摆脱了 的束缚,但由于他是通过 摆脱束缚的,所以束缚 的点必定束缚 ,因而可以直接继承直接支配点。
1.2.3.半支配定理
对于第 类,即 ,这是 的特殊情况。
对于第 类,即 ,其实就是考虑横叉边的情况。
1.3.§ 第一推论——直接支配推论 §
,令 为所有满足 中 最小的点,那么
其实就是将前两个定理 ruá 在了一起,其实在与上面取等的时候,就是 取到 或者 有共同半支配点的时候,即封闭定理的情况。
由这个推论不难看出,对于一个新图 ,其中 ,那么 和 的支配关系是一样的,因为第二种取等情况,可以一直递归至第一种情况,并且一定有 是一个 .
肆、建树 ¶
大致分为几种不同的图:
- 树,自己就是支配树;
- ,则对原图进行拓扑排序,依次确定 ,设当前点为 ,若 ,则所有 的 就是 ;
- 有向图,由直接支配推论,我们可以将 转化为 ,然后在这个 上使用拓扑即可建出支配树。或者使用另外一种方法,将所有点的 找出来之后,使用推论寻找 ,这样可以避免寻找 ,将 的 变成并查集的一般 ,很少会有变态把并查集卡成 的吧?
最复杂情况即一般有向图,时间复杂度 或 .
伍、参考代码 ¶
使用的 的算法,毕竟太懒了......
#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;
}
陆、容易锅的地方 ¶
这些锅可能出现在我的某次考试中,或者原题检测、或者一些其他的时候......
谁知道呢,反正这些地方容易出锅就对了。
- 由于 是以 结尾的,所以有些地方潜伏的 ;
- 多组数据的时候,不止清空 ,还要记得 ;
- 进行 的时候,记得加上 ;
- 注意支配树的根是 ,所以支配树的 上限不只是 ,而是 ;
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现