一、引入
本篇博客参考了这位大佬的博客,写的非常好,让蒟蒻受益匪浅。
支配树是个什么东西呢?
首先我们定义支配点:在一个有向图上,指定一个起点(S),然后如果删除一个点(x)以及他连接的所有边,就无法再从(S)到达另一个点(y),那么(x)就是(y)的支配点。
而支配树,则是一棵满足树上任意一个点的所有祖先都是它的支配点的树。
显然一个点能支配的所有点就是它在支配树上的子树中的点。同时,我们特别定义一个点(x)在支配树上的父亲为(idom(x))
构建支配树,显然可以用暴力枚举断掉每一个点后再(bfs)一遍完成,但这样做是(mathcal O(nm))的,我们需要更高效的算法。
二、DAG上构建支配树
我们先研究简化版的情况:(DAG)上的支配树。
在(DAG)上,考虑如果(y)是点(x)的支配点,那么需要满足断掉(y)后,从(S)无法到达所有可以到达(x)的点(k),也就是说,(y)是所有(k)的支配点,也就是这些(k)在支配树上的公共祖先,那么它们中深度最大的一个,也就是这些(k)的(LCA),就是(idom(x))
于是我们按拓扑序从小到大依次处理,那么当处理到(x)时,所有可达(x)的点一定都已经被加入到支配树中了,就可以(mathcal O(log(n)))得到(idom(x))了
例题1:洛谷P2597 [ZJOI2012]灾难
捕食者的所有食物死亡时它就会死亡,因此导致它死亡的就是反向建图后它的支配点。
于是反向建图并建出支配树后,每种生物的灾难值就是它的子树大小(-1),同时对于最低级生物有多种的情况,我们考虑再新建一种生物作为所有最低级生物的食物,然后以它作为起点。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,first[N],cnt,rt[N],top[N],tot,d[N];
struct node{
int v,nxt;
}e[N<<1];
vector<int> ru[N];
inline void add(int u,int v){e[++cnt].v=v;e[cnt].nxt=first[u];first[u]=cnt;ru[v].push_back(u);}
namespace dt{
int dep[N],pa[N][20],tot,head[N],siz[N];
node d[N<<1];
inline void Add(int u,int v){
dep[v]=dep[u]+1;
pa[v][0]=u;
for(int i=1;i<=19;++i) pa[v][i]=pa[pa[v][i-1]][i-1];
d[++tot].v=v;d[tot].nxt=head[u];head[u]=tot;
}
inline int LCA(int u,int v){
if(dep[u]<dep[v]) swap(u,v);
int t=dep[u]-dep[v];
for(int i=19;i>=0;--i) if(t&(1<<i)) u=pa[u][i];
if(u==v) return u;
for(int i=19;i>=0;--i) if(pa[u][i]!=pa[v][i]) u=pa[u][i],v=pa[v][i];
return pa[u][0];
}
inline void dfs(int u){
siz[u]=1;
for(int i=head[u];i;i=d[i].nxt){
int v=d[i].v;
if(v!=pa[u][0]) dfs(v),siz[u]+=siz[v];
}
}
inline void build(){
for(int i=2;i<=n;++i){
int u=top[i],lca=0;
for(int j=0;j<ru[u].size();++j){
int v=ru[u][j];
if(!lca) lca=v;
else lca=LCA(lca,v);
}
Add(lca,u);
}
dfs(n);
for(int i=1;i<n;++i) printf("%d
",siz[i]-1);
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;++i){
int x;
scanf("%d",&x);
if(!x) add(n+1,i);
else
do{add(x,i);}while(scanf("%d",&x)&&x);
}
++n;
queue<int> q;q.push(n);
for(int i=1;i<=n;++i) d[i]=ru[i].size();
while(!q.empty()){
int u=q.front();q.pop();
top[++tot]=u;
for(int i=first[u];i;i=e[i].nxt){
int v=e[i].v;
if(!(--d[v])) q.push(v);
}
}
dt::build();
return 0;
}
例题2:CF757F Team Rocket Rises Again
三、有向图的支配树
接下来我们介绍一个优秀的解决支配树问题的算法——Lengauer Tarjan算法
首先我们建出原图的(dfs)树并求出每个点的(dfs)序
为了证明一些性质,我们定义(acdot ightarrow b)表示(a)是(b)的祖先,(a+ ightarrow b)表示(a)是(b)的祖先且(a ot=b),并且一下所有点之间的比较都代表它们(dfs)序间的比较。
(dfs)树对于我们的目标来说有两个重要的性质:
-
性质(1):横叉边只从(dfn)较大的点连向(dfn)较小的点。
证明:如果不满足,那么(dfs)时直接会从这条边走,于是这条边就不会是横叉边了
-
性质(2):如果存在两个点(u,v)满足(ule v),那么任意(u ightarrow v)的路径一定经过(u,v)的公共祖先
证明:如果(u,v)是祖先关系显然成立,否则删掉(u,v)的所有公共祖先,那么(u,v)被分离在两个子树中,(u ightarrow v)要跨越子树,能跨越子树的边只有横叉边,而它只能从(dfn)较大的点走向(dfn)较小的点,于是(u)无法到达(v),因此(u ightarrow v)的路径一定经过公共祖先。
接着我们引入一个新的概念:
半支配点
对于任意两点(x,y),如果存在一条从(y)出发到达(x)的路径且路径上除(x,y)以外的任意一点(k)都满足(dfn[k]>dfn[x]),且(y)是所有对(x)满足这一性质的点中(dfn)最小的一个,就称(y)为(x)的半支配点,即(semi(x))。
重要引理:
-
引理(1):(semi(x)+ ightarrow x)
证明:(x)在(dfs)树上的父亲(fa_x)一定也满足(semi)的性质,所以一定有(semi(x)le fa_x),又因为(semi(x))不可能在其他子树上,因为由性质(2)我们知道,(semi(x) ightarrow x)的路径一定经过公共祖先(w)而(w<semi(x)),与定义矛盾,因此(semi(x))一定是(fa_x)的祖先。
-
引理(2):(idom(x)cdot ightarrow semi(x))
证明:如果引理(2)不成立,那么从(semi(x))到(x)的路径就能绕开(idom(x)),与定义不符。
-
引理(3):任意满足(xcdot ightarrow y)的点(x,y)都有(xcdot ightarrow idom(y))或(idom(y)cdot ightarrow idom(x))
证明:如果不成立,则(idom(x)+ ightarrow idom(y)+ ightarrow x+ ightarrow y),于是(idom(y))不支配(x),那么存在路径绕过(idom(y))到达(x),进而到达(y),与定义不符,所以引理(3)成立
求解半支配点
对于点(x),我们找到所有连向(x)的点(y)
- 如果(dfn[y]<dfn[x]),那么(y)满足半支配点的性质,直接更新
- 否则,我们用(y)的所有祖先(z)的(semi(z))来更新(semi(x))
感性理解一下:
-
首先(semi(z))一定存在路径可以绕过(semi(z))与(x)之间的点到达(x),于是(semi(x)le semi(z))
-
其次,考虑(semi(x))到(x)的路径,如果只有一条边,那么会由情况(1)更新,否则,我们取(y)为(x)的前驱,再取点(z)为满足(zcdot ightarrow y)且(z)不是两端的点的最小(z),(一定存在这样的一个点,因为(y)自己就是一个符合条件的(z)),那么(semi(x))到(z)的路径一定绕过了(z)的祖先,于是(semi(x))是(semi(z))的候选点,有(semi(x)ge semi(z))
-
综上所述,我们可以用(semi(z))来更新(semi(x))且没有其他情况。
-
对此,我们可以用带权并查集维护每个点在(dfs)树上到根路径上满足(dfn(semi(z)))最小的(z),然后用它来更新(semi(x))
-
代码如下:(rg)是反图
inline int find(int x){//带权并查集,mi[x]是x到根路径上满足dfn[semi[k]]最小的k
if(f[x]==x) return x;
int t=f[x];f[x]=find(f[x]);
if(dfn[semi[mi[t]]]<dfn[semi[mi[x]]]) mi[x]=mi[t];
return f[x];
}//路径压缩后用路径上的semi更新当前点维护的mi
inline void findidom(){
for(int i=1;i<=n;++i) mi[i]=semi[i]=f[i]=i;
for(int i=n;i>=2;--i){//按dfs序倒序枚举
int u=pos[i],sem=n;
for(int j=rg.first[u];j;j=rg.e[j].nxt){
int v=rg.e[j].v;
if(dfn[v]<dfn[u]) sem=min(sem,dfn[v]);//dfn[v]<dfn[u]直接更新semi
else find(v),sem=min(sem,dfn[semi[mi[v]]]);//否则,用v祖先中semi最小的一个更新semi
}
semi[u]=pos[sem];f[u]=fa[u];//找到semi[u],将它并入并查集中
}
}
用半支配点求解支配点
假设我们已知(semi(x)),(x ot= s),需要求解的是(idom(x))
记(P)为(semi(x))到(x)的所有路径的点集(不包含(semi(x))),取(z)为(P)中满足(dfn(semi(z)))最小的点,我们有以下定理:
- 定理(1):如果(semi(z)ge semi(x))那么(idom(x)=semi(x))
证明:如果最小的(semi(z))都(ge semi(x)),那么也就是说没有(semi(x))的祖先能绕过(semi(x))到达(P)中的点,那么(semi(x))支配(x),故(semi(x)cdot ightarrow idom(x)),根据引理(2):(idom(x)cdot ightarrow semi(x)),于是(idom(x)=semi(x))
-
定理(2):如果(semi(z)<semi(x))那么(idom(x)=idom(z))
感性理解:条件即(semi(z)cdot ightarrow semi(x)cdot ightarrow zcdot ightarrow x),根据引理(3)有(zcdot ightarrow idom(x))或(idom(x)cdot ightarrow idom(z))
- 如果(zcdot ightarrow idom(x)),由引理(2),(idom(x)cdot ightarrow semi(x)),于是(idom(x)cdot ightarrow z),矛盾
- 否则,取(s)到(x)的路径上最后一个(<idom(z))的(w),取(w)最小的一个后继(y)使(idom(z)cdot ightarrow ycdot ightarrow x)。显然(w)与(y)的路径上不能有(v)使(idom(z)cdot ightarrow v+ ightarrow y)(否则(v)抢(y)的位置),那么这条路径只经过(>y)的点,于是(w)满足(semi(y))的条件,(wge semi(y)),于是(semi(y)le w<idom(z)le semi(z)le semi(x)<zle x)
- 注意到(y)一定不会在(semi(x))到(x)的路径上,否则就违反了(semi(z))最小这一前提
- 同时(y)也不再(idom(z))到(semi(x))的路径上,否则我们就能通过到(semi(y))再到(y)再到(z)绕开(idom(z))到达了(z)。
- 再加上(idom(z)cdot ightarrow y),于是只剩下一种选择:(idom(z)=y),因为(y)支配(x)于是(idom(z))支配(x),进而得到(idom(x)=idom(z))。
那么有了这(2)个定理,我们就能够推出(idom(x))了,具体实现看代码注释:
inline int find(int x){//带权并查集,mi[x]是x到根路径上满足dfn[semi[k]]最小的k
if(f[x]==x) return x;
int t=f[x];f[x]=find(f[x]);
if(dfn[semi[mi[t]]]<dfn[semi[mi[x]]]) mi[x]=mi[t];
return f[x];
}//路径压缩后用路径上的semi更新当前点维护的mi
inline void findidom(){
for(int i=1;i<=n;++i) mi[i]=semi[i]=f[i]=i;
for(int i=n;i>=2;--i){//按dfs序倒序枚举
int u=pos[i],sem=n;
for(int j=rg.first[u];j;j=rg.e[j].nxt){
int v=rg.e[j].v;
if(dfn[v]<dfn[u]) sem=min(sem,dfn[v]);//dfn[v]<dfn[u]直接更新semi
else find(v),sem=min(sem,dfn[semi[mi[v]]]);//否则,用v祖先中semi最小的一个更新semu
}
semi[u]=pos[sem];f[u]=fa[u];//找到semi[u],将它并入并查集中
ng.add(semi[u],u);
u=pos[i-1];//此时,所有semi可能是u的点一定已经全部找出(因为dfs序大于dfn[u]的都已经考虑过了)
for(int j=ng.first[u];j;j=ng.e[j].nxt){
int v=ng.e[j].v;
find(v);//u一定是v的祖先,此时取并查集上维护的min就一定能取到(u,v)当前路径上dfn[semi[x]]最小的x
if(semi[mi[v]]==u) idom[v]=u;
//当最小的semi=u时,说明没有点能绕过u来到v,那么u就是v的支配点
else idom[v]=mi[v];
//否则idom[v]就应该是idom[mi[v]]
//但mi[v]的支配点还没有找到,也就无法更新,先记录下来,等全部递归完后,再用idom[v]=idom[idom[v]]更新
}
}
for(int i=2;i<=n;++i){//正序遍历,保证遍历到u时mi[u]的支配点一定已经找到
int u=pos[i];
if(idom[u]!=semi[u]) idom[u]=idom[idom[u]];//idom[u]!=semi[u]说明是上面所说的第二条注释
tr.add(idom[u],u);
}
}
至此,我们终于完成了构建支配树的全过程,或许理解比较难,但它的代码还是比较短的
在这里给出洛谷模板题的代码:
#include<bits/stdc++.h>
using namespace std;
inline int read(){
int x=0,f=1;
char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
return x*f;
}
const int N=3e5+10;
int n,m,tot;
int dfn[N],pos[N],mi[N],fa[N],f[N];
int semi[N],idom[N];
struct node{
int v,nxt;
};
struct graph{
int first[N],cnt;
node e[N];
inline void add(int u,int v){e[++cnt].v=v;e[cnt].nxt=first[u];first[u]=cnt;}
}g,rg,ng,tr;//g:原图 rg:反图 ng:仅保留dfs树与(semi[x],x)的图 tr:支配树
inline void dfs(int u){
dfn[u]=++tot;pos[tot]=u;
for(int i=g.first[u];i;i=g.e[i].nxt){
int v=g.e[i].v;
if(!dfn[v]) fa[v]=u,dfs(v);
}
}
inline int find(int x){//带权并查集,mi[x]是x到根路径上满足dfn[semi[k]]最小的k
if(f[x]==x) return x;
int t=f[x];f[x]=find(f[x]);
if(dfn[semi[mi[t]]]<dfn[semi[mi[x]]]) mi[x]=mi[t];
return f[x];
}//路径压缩后用路径上的semi更新当前点维护的mi
inline void findidom(){
for(int i=1;i<=n;++i) mi[i]=semi[i]=f[i]=i;
for(int i=n;i>=2;--i){//按dfs序倒序枚举
int u=pos[i],sem=n;
for(int j=rg.first[u];j;j=rg.e[j].nxt){
int v=rg.e[j].v;
if(dfn[v]<dfn[u]) sem=min(sem,dfn[v]);//dfn[v]<dfn[u]直接更新semi
else find(v),sem=min(sem,dfn[semi[mi[v]]]);//否则,用v祖先中semi最小的一个更新semu
}
semi[u]=pos[sem];f[u]=fa[u];//找到semi[u],将它并入并查集中
ng.add(semi[u],u);
u=pos[i-1];//此时,所有semi可能是u的点一定已经全部找出(因为dfs序大于dfn[u]的都已经考虑过了)
for(int j=ng.first[u];j;j=ng.e[j].nxt){
int v=ng.e[j].v;
find(v);//u一定是v的祖先,此时取并查集上维护的min就一定能取到(u,v)当前路径上dfn[semi[x]]最小的x
if(semi[mi[v]]==u) idom[v]=u;
//当最小的semi=u时,说明没有点能绕过u来到v,那么u就是v的支配点
else idom[v]=mi[v];
//否则idom[v]就应该是idom[mi[v]]
//但mi[v]的支配点还没有找到,也就无法更新,先记录下来,等全部递归完后,再用idom[v]=idom[idom[v]]更新
}
}
for(int i=2;i<=n;++i){//正序遍历,保证遍历到u时mi[u]的支配点一定已经找到
int u=pos[i];
if(idom[u]!=semi[u]) idom[u]=idom[idom[u]];//idom[u]!=semi[u]说明是上面所说的第二条注释
tr.add(idom[u],u);
}
}
int siz[N];
inline void dfs_tr(int u){//遍历支配树求siz
siz[u]=1;
for(int i=tr.first[u];i;i=tr.e[i].nxt){
int v=tr.e[i].v;
dfs_tr(v);siz[u]+=siz[v];
}
}
int main(){
n=read();m=read();
for(int i=1,u,v;i<=m;++i){
u=read();v=read();
g.add(u,v);rg.add(v,u);
}
dfs(1);//建出dfs树
findidom(); //建出支配树
dfs_tr(1);
for(int i=1;i<=n;++i) printf("%d ",siz[i]);
return 0;
}