支配树口胡

感性理解支配树

0. 前言

  • 代码都在最后

1. 支配树简介

支配树的定义:

对于一张有向图 \(\mathrm G\),指定一起点 \(s\) .(或许也被称作流程图?)

点 \(u\) 支配点 \(v\)(即 \(u\) 为 \(v\) 的 支配点)当且仅当 \(s\) 到达 \(v\) 的所有路径都必须通过 \(u\)(这意味着如果去掉点 \(\textbf{\textit{u}}\) 则从 \(\textbf{\textit{s}}\) 不可能到达 \(\textbf{\textit{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}\) 了

代码也很好写,用带权并查集可以轻松维护最小值 .

2. 题

题倒是蛮少的,出出来也是模板

感觉出题空间挺大吧

1. Codeforces 757F

题意:略

DAG 上支配树板子

2. Codechef GRAPHCNT

题意:

问有多少个点对 \((x,y)\),满足存在路径 \(1\to x\),\(1\to y\) 满足 两条路径公共点只有点 \(1\)

就是支配树上 LCA 为 \(1\) 的点对,随便统计下就可 .

代码略

就是支配树上lca为1的点的点对嘛……

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;
}
上一篇:P7469 [NOI Online 2021 提高组] 积木小赛 题解


下一篇:餐巾计划问题