支配树
在一个有向图中,有一个起点R,对于任意点W,对于R->W的任意路径都经过点P,则称P为W的支配点。设idom[i]表示距离i最近的支配点。在原图基础上,idom[i]向i连边构成一颗新树,称为支配树
支配树的性质
1.支配树是以R为根的一棵树
2.对于任意点i,到根r路径上经过的点集{xi}是原图上r->i的必经点
3.对于任意的i,它是子树中每个点的必经点
半必经点
在dfs搜索树中,对于一个节点Y,存在某个点X能够通过一系列点pi(不包含X和Y)到达点Y且∀i dfn[i]>dfn[Y],我们就称X是Y的半必经点,记做semi[Y]=X
通俗理解:semi[x]就是x在dfs树中所有祖先中z,能不经过 z 和 x 之间的树上的点而到达 x 的点中深度最小的。
半必经点性质
- semi[x]一定是x的祖先
- semi[x]一定是确定的
- 半必经点不一定是必经点
- semi[x]深度不小于idom[x],即idom[x]在semi[x]祖先链上
计算semi
对于点x,有边(y,x)
-
若dfn[y]<dfn[x](树边或前向边) 且dfn[y]<dfn[semi[x]] ,semi[x]=y
-
若dfn[y]>dfn[x](后向边或横叉边),找到y的一个祖先semi值最小的z且dfn[z]>dfn[x],用semi[z]更新semi[x]
计算idom
设点集 P 是 semi(x)(不包括) 到 x 路径上经过的点,t 是点集 P 中 semi 最小的点。
-
如果 semi(x)=semi(t),那么 idom(x)=semi(x)。 只需证明semi(x)是 x 的必经点。用力感受一下,如果能绕过 semi(x) 的话,肯定会先跳到点集P中。
-
如果 semi(x)≠semi(t),那么 idom(x)=idom(t)。 首先证明 idom(t)是 x 的必经点。如果 idom(t) 不能用的话,在 idom(t) 到 t 的那一段都不能用,由于 t 到 x 的那段 semi不够小,所以绕不过。
然后证明 idom(x)的深度不能更大。反证,如果深度更大,那么首先能到 idom(t)。此时利用 semi(x)→x 和 idom(t)→t 两个传送门,删除 idom(t) 和 x 之间的任意一个结点也无济于事。(注意到 t 在 semi(x) 和 x 之间)
代码
//#include <bits/stdc++.h>
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <queue>
#include <stack>
#include <set>
#include <map>
#include <algorithm>
#include <cmath>
#include <functional>
#define INF 999999999
#define N 200010
#define M 300010
#define ll long long
using namespace std;
struct A
{
int s,e,nxt;
}way[M];
int n,m,cnt,root,ans[N];
int id[N],dfn[N],semi[N],idom[N];//id[x]:dfn为x的点;semi[x]:x的半支配点;idom[x]:x的支配点
int f[N],val[N];//f[x]:用于记录带权并查集的父亲结点;val[x]:带权并查集的权,记录x到f[x](包括x和f[x])中semi最小的点
int head[N],fa[N];//fa[x]记录x在dfs树的父亲结点
//reidom[x]存idom为x的点;resemi[x]存semi为x的点;pre[x]存原图中x的前驱(只要有直连边到x)
vector <int> reidom[N],pre[N],resemi[N];
void get_dfn(int x)//求dfn序,记录dfn,id,fa
{
int l;
cnt++;
dfn[x]=cnt;
id[cnt]=x;
for(int i=head[x];i!=0;i=way[i].nxt)
{
l=way[i].e;
if(dfn[l]==0)
{
get_dfn(l);
fa[l]=x;
}
}
return;
}
int find(int x)//带权并查集的路径压缩操作,返回值其实没用
{
int k;
if(f[x]==x) return x;
else
{
k=find(f[x]);
if(dfn[semi[val[f[x]]]]<dfn[semi[val[x]]])
val[x]=val[f[x]];
f[x]=k;
return f[x];
}
}
void tarjan()
{
int k,l;
for(int i=cnt;i>=1;i--)
{
k=id[i];//按dfn序逆序操作(目前k的dfn序是最大的)
for(int j=0;j<pre[k].size();j++)
{
l=pre[k][j];//有一条边(l,k)
if(dfn[l]==0) continue;//可能有root走不到的点
//其实不需要这个判断,都写成if中的操作也是对的
if(dfn[l]>dfn[k])//如果l已经操作过,说明semi[l]已经确定了
{
find(l);//更新l的并查集,让他直接连到并查集头目(路径压缩)
//用semi[val[l]]-->val[l]-->l-->k来更新semi[k],这条路径上只有起点semi[val[l]]的dfn可能小于k
if(dfn[semi[val[l]]]<dfn[semi[k]])
semi[k]=semi[val[l]];
}
else//如果l没有操作过,用l来更新semi[k]
{
if(dfn[l]<dfn[semi[k]])
semi[k]=l;
}
}
//此时semi[k]已更新完毕
resemi[semi[k]].push_back(k);
f[k]=fa[k];
//此时如果有点l满足semi[l]=fa[k],那么l并查集中的头目为fa[k](fa[k]可以到l,而且dfn大于dfn[k]的都尚未访问)
for(int j=0;j<resemi[fa[k]].size();j++)
{
l=resemi[fa[k]][j];
find(l);
if(semi[val[l]]==fa[k]) idom[l]=fa[k];//如果semi[l]==fa[k]且semi[val[l]]==fa[k],那么idom[l]=fa[k]
else idom[l]=val[l];//如果semi[l]==fa[k]且semi[val[l]]!=fa[k],那么idom[l]=idom[val[l]],这里相当于暂时存一下
}
}
for(int i=2;i<=cnt;i++)//找当时semi[l]==fa[k]且semi[val[l]]!=fa[k]的点,按dfn从小到大访问
{
k=id[i];
if(idom[k]!=semi[k])
idom[k]=idom[idom[k]];
}
return;
}
void sum(int x)
{
int l;
ans[x]++;
for(int i=0;i<reidom[x].size();i++)
{
l=reidom[x][i];
sum(l);
ans[x]+=ans[l];
}
return;
}
int main()
{
cin >> n >> m;
for(int i=1;i<=m;i++)
{
scanf("%d%d",&way[i].s,&way[i].e);
way[i].nxt=head[way[i].s];
head[way[i].s]=i;
pre[way[i].e].push_back(way[i].s);
}
root=1;
for(int i=1;i<=n;i++)
{
semi[i]=i;
f[i]=i;
val[i]=i;
}
get_dfn(root);
tarjan();
for(int i=1;i<=n;i++)//建立支配树
{
if(dfn[i])
{
reidom[idom[i]].push_back(i);
}
}
sum(root);
for(int i=1;i<=n;i++)
printf("%d ",ans[i]);
return 0;
}
参考
https://www.luogu.com.cn/problem/P5180
https://www.cnblogs.com/Mr-Spade/p/10106905.html
https://zerol.me/2018/10/22/dominator-tree/
https://www.cnblogs.com/BeyondStars/p/12380664.html