支配树学习笔记
一、引入
本篇博客参考了这位大佬的博客,写的非常好,让蒟蒻受益匪浅。
支配树是个什么东西呢?
首先我们定义支配点:在一个有向图上,指定一个起点\(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\n",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\)序
为了证明一些性质,我们定义\(a\cdot \rightarrow b\)表示\(a\)是\(b\)的祖先,\(a+\rightarrow b\)表示\(a\)是\(b\)的祖先且\(a\not=b\),并且一下所有点之间的比较都代表它们\(dfs\)序间的比较。
\(dfs\)树对于我们的目标来说有两个重要的性质:
-
性质\(1\):横叉边只从\(dfn\)较大的点连向\(dfn\)较小的点。
证明:如果不满足,那么\(dfs\)时直接会从这条边走,于是这条边就不会是横叉边了
-
性质\(2\):如果存在两个点\(u,v\)满足\(u\le v\),那么任意\(u\rightarrow v\)的路径一定经过\(u,v\)的公共祖先
证明:如果\(u,v\)是祖先关系显然成立,否则删掉\(u,v\)的所有公共祖先,那么\(u,v\)被分离在两个子树中,\(u\rightarrow v\)要跨越子树,能跨越子树的边只有横叉边,而它只能从\(dfn\)较大的点走向\(dfn\)较小的点,于是\(u\)无法到达\(v\),因此\(u\rightarrow v\)的路径一定经过公共祖先。
接着我们引入一个新的概念:
半支配点
对于任意两点\(x,y\),如果存在一条从\(y\)出发到达\(x\)的路径且路径上除\(x,y\)以外的任意一点\(k\)都满足\(dfn[k]>dfn[x]\),且\(y\)是所有对\(x\)满足这一性质的点中\(dfn\)最小的一个,就称\(y\)为\(x\)的半支配点,即\(semi(x)\)。
重要引理:
-
引理\(1\):\(semi(x)+ \rightarrow x\)
证明:\(x\)在\(dfs\)树上的父亲\(fa_x\)一定也满足\(semi\)的性质,所以一定有\(semi(x)\le fa_x\),又因为\(semi(x)\)不可能在其他子树上,因为由性质\(2\)我们知道,\(semi(x)\rightarrow x\)的路径一定经过公共祖先\(w\)而\(w<semi(x)\),与定义矛盾,因此\(semi(x)\)一定是\(fa_x\)的祖先。
-
引理\(2\):\(idom(x)\cdot \rightarrow semi(x)\)
证明:如果引理\(2\)不成立,那么从\(semi(x)\)到\(x\)的路径就能绕开\(idom(x)\),与定义不符。
-
引理\(3\):任意满足\(x\cdot \rightarrow y\)的点\(x,y\)都有\(x\cdot \rightarrow idom(y)\)或\(idom(y)\cdot \rightarrow idom(x)\)
证明:如果不成立,则\(idom(x)+\rightarrow idom(y)+\rightarrow x+\rightarrow 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\)为满足\(z\cdot \rightarrow 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 \not= 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\rightarrow idom(x)\),根据引理\(2\):\(idom(x)\cdot \rightarrow semi(x)\),于是\(idom(x)=semi(x)\)
-
定理\(2\):如果\(semi(z)<semi(x)\)那么\(idom(x)=idom(z)\)
感性理解:条件即\(semi(z)\cdot\rightarrow semi(x)\cdot\rightarrow z\cdot \rightarrow x\),根据引理\(3\)有\(z\cdot \rightarrow idom(x)\)或\(idom(x)\cdot \rightarrow idom(z)\)
- 如果\(z\cdot\rightarrow idom(x)\),由引理\(2\),\(idom(x)\cdot \rightarrow semi(x)\),于是\(idom(x)\cdot \rightarrow z\),矛盾
- 否则,取\(s\)到\(x\)的路径上最后一个\(<idom(z)\)的\(w\),取\(w\)最小的一个后继\(y\)使\(idom(z)\cdot\rightarrow y\cdot \rightarrow x\)。显然\(w\)与\(y\)的路径上不能有\(v\)使\(idom(z)\cdot\rightarrow v+\rightarrow y\)(否则\(v\)抢\(y\)的位置),那么这条路径只经过\(>y\)的点,于是\(w\)满足\(semi(y)\)的条件,\(w\ge semi(y)\),于是\(semi(y)\le w<idom(z)\le semi(z)\le semi(x)<z\le x\)
- 注意到\(y\)一定不会在\(semi(x)\)到\(x\)的路径上,否则就违反了\(semi(z)\)最小这一前提
- 同时\(y\)也不再\(idom(z)\)到\(semi(x)\)的路径上,否则我们就能通过到\(semi(y)\)再到\(y\)再到\(z\)绕开\(idom(z)\)到达了\(z\)。
- 再加上\(idom(z)\cdot\rightarrow 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;
}