支配树口胡
感性理解支配树
0. 前言
- 代码都在最后
upd 2022/2/23. 增加一道例题(小W与施工),加上算法名(鬼知道我为啥没加算法名)
1. 支配树简介
支配树的定义:
对于一张有向图 \(\mathrm G\),指定一起点 \(s\) .(或许也被称作流程图?)
点 \(u\) 支配点 \(v\)(即 \(u\) 为 \(v\) 的 支配点)当且仅当 \(s\) 到达 \(v\) 的所有路径都必须通过 \(u\)(这意味着如果去掉点 \(\pmb u\) 则从 \(\pmb s\) 不可能到达 \(\pmb v\))
将所有点 \(u\) 向其最近支配点(其定义在后面)连边,不难发现,会得到一棵树,这棵树就叫做图 \(\mathrm G\) 的支配树(Dominator Tree).
2. 支配树的求法
2.1. 树的支配树
即为其本身(
2.2. DAG 的支配树
题目:[ZJOI2012]灾难
由于不吃任何东西的生物可能有多种,先加一个虚拟节点连向所有不吃东西的生物 .
不难发现,\(u\) 灭绝当且仅当 \(u\) 的所有直接食物的 LCA 灭绝 .
按支配树定义,把 \(u\) 的所有直接食物的 LCA 和 \(u\) 连边,支配树就建好力 .
只需按拓扑序依次加边,即可建成支配树 .
不难发现,对于生物 \(u\),\(u\) 在支配树上的子树大小即为题目要求的答案 .
3.3. 任意有向图的支配树
声明:内容都是感性理解,想看详细证明可见:https://www.cnblogs.com/meowww/archive/2017/02/27/6475952.html
记号声明:
- \(\operatorname{dfn}(u)\) 为 \(u\) 的图的 DFS 序中 \(u\) 的编号 .
- \(u\mathrm{\, dom\,}v\) 表示 \(u\) 是 \(v\) 的支配点 .
- \(\operatorname{idom}(u)\) 为 \(u\) 的最近支配点 .
- \(\operatorname{sdom}(u)\) 为 \(u\) 的半支配点 .
先放上 \(\operatorname{idom}\) 和 \(\operatorname{sdom}\) 的定义:
最近支配点(immediate dominator)的定义:
在支配 \(u\) 的点中,如果一个支配点 \(v\neq u\) 满足 \(v\) 被 \(u\) 剩下的所有非平凡支配点(即不是自己或起点的支配点)支配,则这个 \(v\) 称作 \(u\) 的 最近支配点,记作 \(\operatorname{idom}(u)\) .
不难发现,最近支配点唯一 .
半支配点(semi-dominator)的定义:
在 \(u\) 的祖先中,通过非树枝边可以到打 \(u\) 深度最小的祖先,称作 \(u\) 的半必经点,记做 \(\operatorname{sdom}(u)\)(也可记 \(\operatorname{semi}(u)\)).
半支配点显然唯一 .
算法流程:
首先,求出原图的随便一棵 DFS 树,由此我们可以把图上的边分为四类:
- 树枝边:在 DFS 树上的边(其余边称作非树枝边).
- 前向边:从 DFS 树上的祖先指向后代 .
- 后向边:从 DFS 树上的后代指向祖先 .
- 横叉边:从一棵子树内指向另一棵子树内 .
先放上一定理:
对于 \(u\neq S\)(\(S\) 是起点),\(v\) 为所有满足 \(\operatorname{sdom}(u)\) 可通过一条树枝边到达 \(v\),\(v\) 又可通过一条树枝边到达 \(u\),且 \(\operatorname{sdom}(u)\neq v\)
\[\large\operatorname{idom}(u)=\begin{cases}\operatorname{sdom}(u)&\operatorname{sdom}(u)=\operatorname{sdom}(v)\\\operatorname{idom}(v)&\operatorname{sdom}(u)\neq\operatorname{sdom}(v)\end{cases} \]
证明可以感性理解,即为按最近支配点是否可以绕过去到达 \(u\) 点分类讨论 .
从这个定理至少可以看出,求出每个点的 \(\operatorname{sdom}\) 就可求出每个点的 \(\operatorname{idom}\) .
那么 \(\operatorname{sdom}\) 怎么求呢?
我们有
对于任意 \(w\neq S\)
\[\large \begin{aligned}\operatorname{sdom}(w)=\min\{\{v\mid (v,w)\in E,\, v<w\}\cup\{\operatorname{sdom}(u)\mid (v,w)\in E,\,u>w,\,t(u,v)\}\}\end{aligned} \]其中 \(t(u,v)\) 指在 DFS 树上 \(u\) 与 \(v\) 可通过一条树枝边互通 .
这个也可以感性理解,前面那个集合是后向边和横叉边,后面那个集合是前向边和树枝边 .
根据上面那个东西就能轻♪松求出 \(\operatorname{sdom}\) 了
代码也很好写,用带权并查集可以轻松维护最小值 .
这个玩意就叫做 Lengauer-Tarjan 算法
2. 题
题倒是蛮少的,出出来也是模板
感觉出题空间挺大吧
1. Codeforces 757F
题意:略
DAG 上支配树板子
2. Codechef GRAPHCNT
题意:
问有多少个点对 \((x,y)\),满足存在路径 \(1\to x\),\(1\to y\) 满足 两条路径公共点只有点 \(1\)
就是支配树上 LCA 为 \(1\) 的点对,随便统计下就可 .
3. 小W与施工
题意:
一个无环流程图(含起点 \(s\),终点 \(t\) 的 DAG),\(q\) 次询问,每次去掉一条边,问 \(s\) 到 \(t\) 的最短路是否发生改变 .
考虑 \(s\) 到 \(t\) 的最短路图,于是问题变成删边判连通性 .
这个可以 DAG 上支配树做到 \(O(n\log n)\) .
此处可以 tarjan 求割边做到线性 .
Code
1. 灾难 代码(DAG 的支配树)
using namespace std;
typedef long long ll;
const int N=100005,M=2*N,MOD=1e9+7;
int head[N],shead[N],ver[M],nxt[M],tot=-1; // 链式前向星相关
int n,m,deg[N]; // 输入及拓扑相关
int father[N],siz[N]; // 支配树相关,father[u] 就是 u 在支配树上的父亲
int dep[N],,f[N][33]; // 倍增 LCA 相关(这里因为倍增方便就用倍增力)
void addedge(int head[],int u,int v){ver[++tot]=v; nxt[tot]=head[u]; head[u]=tot;}
int lca(int p1,int p2) // 倍增 LCA
{
if (dep[p1]<dep[p2]) swap(p1,p2);
for (int i=18;i>=0;i--)
if (dep[f[p1][i]]>=dep[p2]) p1=f[p1][i];
if (p1==p2) return p2;
for (int i=18;i>=0;i--)
if (f[p1][i]!=f[p2][i]) p1=f[p1][i],p2=f[p2][i];
return f[p1][0];
}
void topsort() // 拓扑排序
{
queue<int> q;
for (int i=1;i<=n;i++)
if (!deg[i]) q.push(i),father[i]=0; // 在入队时顺便连上虚拟节点
while (!q.empty())
{
int u=q.front(),fa=father[u]; q.pop();
addedge(shead,fa,u);
f[u][0]=fa; dep[u]=dep[fa]+1;
for (int i=1;i<=22;i++) f[u][i]=f[f[u][i-1]][i-1]; // 倍增 LCA 预处理
for (int i=head[u];~i;i=nxt[i])
{
int v=ver[i]; --deg[v];
if (!deg[v]) q.push(v);
if (father[v]==-1) father[v]=u; // 所有直接食物的 LCA 即为父亲
else father[v]=lca(father[v],u);
}
}
}
void dfs(int x) // 统计答案
{
siz[x]=1;
for (int i=shead[x];~i;i=nxt[i]){dfs(ver[i]); siz[x]+=siz[ver[i]];}
}
int main()
{
memset(head,-1,sizeof head); memset(shead,-1,sizeof shead);
memset(father,-1,sizeof father);
scanf("%d",&n);
for (int i=1,x;i<=n;i++)
while (~scanf("%d",&x)&&x) addedge(head,x,i),++deg[i];
topsort(); dfs(0);
for (int i=1;i<=n;i++) printf("%d\n",siz[i]-1); // -1 是为了去掉自己
return 0;
}
2. 【模板】支配树
typedef long long ll;
const int N=20500,M=20*N;
int n,m;
int head[N],shead[N],ihead[N],rhead[N],ver[M],nxt[M],tot=-1; // 链式前向星相关
int siz[N],father[N],fa[N]; // 并查集与支配树相关
int dfn[N],id[N],low[N],sdom[N],idom[N],cnt; // 支配点相关
void addedge(int head[],int u,int v){ver[++tot]=v; nxt[tot]=head[u]; head[u]=tot;}
void init(int n){for (int i=1;i<=n;i++) sdom[i]=low[i]=fa[i]=i;}
void dfs1(int u) // dfs 树相关
{
dfn[u]=++cnt; id[cnt]=u;
for (int i=head[u];~i;i=nxt[i])
{
int v=ver[i];
if (dfn[v]) continue;
father[v]=u; dfs1(v);
}
}
int get(int u) // 并查集
{
if (u==fa[u])return u;
int root=get(fa[u]);
if (dfn[sdom[low[fa[u]]]]<dfn[sdom[low[u]]]) low[u]=low[fa[u]];
return fa[u]=root;
}
void tarjan()
{
int v;
for (int i=cnt;i>1;i--) // 从叶子节点开始求 sdom
{
int u=id[i];
for (int j=rhead[u];~j;j=nxt[j])
{
v=ver[j];
if (!dfn[v]) continue;
get(v);
if (dfn[sdom[low[v]]]<dfn[sdom[u]]) sdom[u]=sdom[low[v]];
}
addedge(shead,sdom[u],u);
fa[u]=father[u]; //合并
u=father[u];
for (int j=shead[u];~j;j=nxt[j]) // 求 idom
{
v=ver[j]; get(v);
if (sdom[low[v]]==u) idom[v]=u;
else idom[v]=low[v];
}
shead[u]=-1; //清空父节点的边
}
for (int i=2;i<=cnt;i++)
{
int u=id[i];
if (idom[u]!=sdom[u]) idom[u]=idom[idom[u]]; // 用引理调整 idom
}
}
void dfs2(int u) // 统计
{
siz[u]=1;
for(int i=ihead[u];~i;i=nxt[i])
{
int v=ver[i];
dfs2(v);
siz[u]+=siz[v];
}
}
int main()
{
memset(head,-1,sizeof head);
memset(ihead,-1,sizeof ihead);
memset(rhead,-1,sizeof rhead);
memset(shead,-1,sizeof shead);
scanf("%d%d",&n,&m);
for (int i=1,u,v;i<=m;i++){scanf("%d%d",&u,&v); addedge(head,u,v); addedge(rhead,v,u);}
init(n); // !!!!!!
dfs1(1);
tarjan();
for (int i=2;i<=n;i++)
if (idom[i]) addedge(ihead,idom[i],i);
dfs2(1);
for (int i=1;i<=n;i++) printf("%d ",siz[i]);
return 0;
}
以下是博客签名,正文无关
本文来自博客园,作者:Jijidawang,转载请注明原文链接:https://www.cnblogs.com/CDOI-24374/p/15083198.html
版权声明:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议(CC BY-NC-SA 4.0)进行许可。看完如果觉得有用请点个赞吧 QwQ