支配樹學習筆記


支配樹(dominator tree) 學習筆記

學習背景

本來本蒟蒻都不知道有一個東西叫支配樹……pkuwc前查某位的水表看見它的大名,甚感恐慌啊。不過好在pkuwc5道題(嗯?)都是概率期望計數,也不知是好還是不好,我在這些方面也只是不好不差……扯遠了。

考掛之后也沒什么心思干別的,想起支配樹這個東西,於是打算學一下。

 

技能介紹(霧)

支配樹是什么?不如直接講支配樹的性質,從性質分析它的定義。

先大概講一下它是來求什么的。

問題:我們有一個有向圖(可以有環),定下了一個節點為起點s。現在我們要求:從起點s出發,走向一個點p的所有路徑中,必須要經過的點有哪些{xp}。

換言之,刪掉{xp}中的任意一個點xpi以及它的入邊出邊,都會使s無法到達p。

我們有一種顯然的O(nm)的方法:枚舉+BFS。

現在我們學習構造圖的支配樹,它是一種復雜度更優秀的做法。

性質:

  1. 它是一棵樹(這不廢話),根節點是我們選定的起點s。
  2. 對於每個點i,它到根的鏈上的點集就是對於它的必經點集{xi}。
  3. 對於每個點i,它是它的支配樹上的子樹內的點的必經點。

所以對於上面的問題,把支配樹摳出來就可以了。

 

算法原理

先來看一下兩種比較簡單的情況。不妨假設從s出發可以到達圖的所有點,不失一般性。

顯而易見的,樹就是自己的支配樹……

有向無環圖(DAG)

DAG上的問題當然要靠拓撲序來搞!

我們利用拓撲序做。對於一個點,所有能到達它的點在支配樹中的lca,就是它支配樹中的父親。

用倍增求lca可以做到O(nlogn)。

比如說 ZJOI2012 災難

答案就是支配樹上的size。

當時這道題好像也挺難……誰能想到新建樹啊……

 

一般有向圖

注:下面的一切涉及大小的都是用dfn做比較的,不然太丑了……

顯然支配具有傳遞性。

先隨便搞出一棵dfs樹,用dfn[x]表示x在dfs序的哪里。

dfs樹一個重要性質:若v,w是圖中節點且dfn[v]<=dfn[w],則任意從v到w的路徑必然包含它們在dfs樹中的一個公共祖先。

定義:semi[x]叫x的半支配點。定義如下:

semi[x]=min{v | 有路徑v=v0, v1, ..., vk=x使得dfn[vi]>dfn[x]對1<=i<=k-1成立}.(掐頭去尾,都走的dfn大於它的點)

當然中間沒有點的話semi[x]就是它dfs樹上的父親。

semi有一些性質,具體可以參見這道題:cogs2117 DAGCH,解法在下面給出。

題中的superior vertex就是semi。

定義:idom[x]表示支配x的點中深度最深的點,叫x的支配點,也叫idom[x]支配了x。idom[x]就是x在支配樹上的父親。

顯然有下面的性質:

    1. 每個點的半支配點是唯一的。
    2. 一個點的半支配點必定是它在dfs樹上的祖先,dfn[semi[x]]<dfn[x]。
    3. 半支配點不一定是x的支配點。
    4. semi[x]的深度不小於idom[x]的深度,即idom[x]在semi[x]的祖先鏈上。
    5. 設節點v,w滿足v->w。則v->idom[w]或者idom[w]->idom[v](a->b表示a在b的祖先鏈上)。

性質5證明:設x是idom[w]的一個完全后代,且同時是v的完全祖先,是idom[v]的后代。則必然有一條從s到v不經過x的路徑。將這條路徑和從v到w的樹上路徑連接起來,我們就得到了一條從s到w不經過x的路徑,矛盾。因此idom[w]要么是v的后代,要么是v的祖先,就要是idom[v]的祖先。

求出semi之后我們把dfs樹上的點保留,和邊(semi[i] -> i)。

現在這張圖已經是一個DAG了,顯然已經可以用上面的方法寫。

但是你已經求出了semi,求idom就有種更快的方法。(semi怎么求后面有講)

定理:idom[x]和semi[x]的關系(如何用semi[x]優雅地得到idom[x])

  • 定義集合{P}表示dfs樹中路徑(semi[x],x)上的點集(不包括semi[x])。
  • 找到{P}中semi的dfn最小的點,記為z。
  • 如果z的semi和x的一樣,則idom[x]=semi[x]。
  • 否則 idom[x]=idom[z]。

對黑字的一些理解:

 

第一行。

  • 由性質4,只要證明semi[x]支配了x就可以了。(感性一下還是很好證明的?)
  • 考慮一條(s => x)的鏈,設w是鏈上最后一個w<=semi[x]的點。如果不存在,那么就肯定支配了。
  • 設y是w后第一個y>=semi[x]的點。則有semi[x]<=y<x;
  • 來看一下路徑(w => y) = {w,p1,p2,p3,……,pk,y},一定有pi>y。
  • 證明:若pi<y,則dfs樹就會變成pi->y->x而不是semi[x]->x了。
  • 於是有semi[y]<=w,因為由semi定義w可能是y的半支配點。
  • 又因為w<semi[x] 所以semi[y]<=semi[x]。
  • 又由有y->x的鏈,所以semi[x]<=semi[y]。
  • 因為y不是semi[x]的完全后代,所以y=semi[x]就順理成章了。
  • 因為鏈是任意的,所以semi[x]支配了x。

第二行

  • 首先一定有idom[z]<=semi[x]<=z<=x。
  • 由性質2和性質4,idom[x]一定是z的完全祖先。
  • 再綜合一下性質5,可以否定第二種情況idom[z]->idom[idom[x]],只存在idom[x]->idom[z]。所以只要證明idom[z]支配了x,就可以證明idom[z]=idom[x]。
  • 還是一樣的,我們考慮一條鏈(s=>x),同樣設w是鏈上最后一個w<=semi[x]的點。如果不存在,那么就肯定支配了。
  • 同樣設y是w后第一個y>=semi[x]的點。則有semi[x]<=y<x,idom[z]<=y<=z<=x;
  • 同樣看路徑(w => y) = {w,p1,p2,p3,……,pk,y},一定有pi>y。證明同上。
  • 所以依舊有semi[y]<=w。
  • 由性質4,可得不等式semi[y]<=w<=idom[z]<=semi[z]。
  • 因為y不是semi[x]的完全后代,且y不可能既是z的祖先,又是idom[z]的完全后代,因為此時會有路徑(s=>y)(不包含idom[z])+(y=>z)=(s=>z)但會避開idom[z],與idom[z]定義矛盾。
  • 由於idom[z]->y->z->x並且idom[z]->y->x,所以唯一的可能就是idom[z]=y。
  • 所以idom[z]必定位於s到x的路徑上。因為路徑是任意的,所以idom[z]支配了x。

寫這兩點好累啊……

(看不懂?沒事,結論和代碼都好背)

很顯然兩行黑字包含了所有情況……

 

 

那么如何用semi推idom我們已經知道了,下面就看如何求semi。

比較大小同樣按照dfn為准。

定理:對任意節點y≠s,有點集{x|(x,y)∈E}。

若x<y,則semi[y]=min(x)。

若x>y,則semi[y]=min({semi[z]|z>y且存在鏈z->y})。

 

這個的證明……很騷……真的很騷……

定理可以簡化為:semi[y]=min({x|(x,y)∈E} ∪ {semi[z] | z>y,z->x,(x,y)∈E})

證明:令g=等式右邊。

證1:semi[y]<=g。

如果是(g,y)∈E,根據semi定義,semi[y]至多是g,等式成立。

如果是第二種情況,則g=semi[z],z>y,z->x,(x,y)∈E。由semi定義,存在路徑g=v0, v1, ..., vk=z使得vi>z對1<=i<=k-1成立

而dfs樹上的路徑(z=v0,v1,v2,…,vk=y)滿足vi>=z>y成立。所以路徑(g=v0,v1,v2,…,vk=y)使得vi>y對1<=i<=k-1成立。

所以g也可以做y的semi,semi[y]<=g。

證2:semi[y]>=g。

圖中肯定存在這么一條路徑 (semi[x]=v0,v1,v2,…,vk=y)使得vi>y對1<=i<=k-1成立。

若k=1,則(g,x)∈E,在第一種情況內。

若k>1,設w是dfn[w]>1且存在(w=>vk-1)的最小值,很明顯它一定存在。

很顯然對於1<=i<=j-1,vi>vj(不然就選i了嘛)。

所以semi[x]>=semi[vj]>=g,即semi[x]>=g。

經過上面兩番證明,semi[y]=g也是水到渠成的了。

(還是看不懂?沒關系,結論代碼依然好背)

附上上面那題的代碼

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <vector>
#include <cmath>
#include <map>
#include <set>
#define LL long long
#define FILE "dagch"
using namespace std;

const int N = 200010;
struct Node{int to,next;}E[N<<1];
int n,m,q,head[N],tot,dfn[N],clo,rev[N],fa[N],semi[N],Ans[N];
vector<int>G[N];
struct Union_Merge_Set{
  int fa[N],Mi[N];
  inline void init(){
    for(int i=0;i<=n;++i)
      fa[i]=Mi[i]=semi[i]=i;
  }
  inline int find(int x){
    if(x==fa[x])return x;
    int fx=fa[x],y=find(fa[x]);
    if(dfn[semi[Mi[fx]]]<dfn[semi[Mi[x]]])Mi[x]=Mi[fx];
    return fa[x]=y;
  }
}uset;

inline int gi(){
  int x=0,res=1;char ch=getchar();
  while(ch>'9' || ch<'0')res^=ch=='-',ch=getchar();
  while(ch>='0'&&ch<='9')x=x*10+ch-48,ch=getchar();
  return res?x:-x;
}

inline void link(int u,int v){
  E[++tot]=(Node){v,head[u]};
  head[u]=tot;
}

inline void tarjan(int x){
  dfn[x]=++clo;rev[clo]=x;
  for(int i=0,j=G[x].size();i<j;++i)
    if(!fa[G[x][i]])
      fa[G[x][i]]=x,tarjan(G[x][i]);
}

inline void build(){
  for(int i=n;i>=2;--i){
    int y=rev[i],tmp=n;
    for(int e=head[y];e;e=E[e].next){
      int x=E[e].to;if(!dfn[x])continue;
      if(dfn[x]<dfn[y])tmp=min(tmp,dfn[x]);
      else uset.find(x),tmp=min(tmp,dfn[semi[uset.Mi[x]]]);
    }
    uset.fa[y]=fa[y];semi[y]=rev[tmp];
    Ans[rev[tmp]]++;
  }
}

inline void solve(){
  n=gi();m=gi();q=gi();fa[1]=1;
  for(int i=1;i<=m;++i){
    int u=gi(),v=gi();
    link(v,u);
    G[u].push_back(v);
  }
  uset.init();
  for(int i=1;i<=n;++i)
    if(G[i].size())
      sort(G[i].begin(),G[i].end());
  tarjan(1);build();
  for(int i=1;i<=q;++i)
    printf("%d ",Ans[gi()]);
  printf("\n");
  for(int i=0;i<=n;++i){
    G[i].clear();head[i]=0;
    Ans[i]=semi[i]=fa[i]=0;
  }
  clo=tot=0;
}

int main(){
  freopen(FILE".in","r",stdin);
  freopen(FILE".out","w",stdout);
  int Case=gi();while(Case--)solve();
  fclose(stdin);fclose(stdout);
  return 0;
}
DAGCH

 

具體實現

算法名叫:Lengauer Tarjan算法,顧名思義是由Lengauer和Tarjan提出的(%Tarjan)。

論文里說:快速支配點算法包含三個部分。

第一步:對原圖做一邊dfs,找出dfs樹不提。

“首先,對輸入的流程圖G=(V,E,r)進行從r開始的深度優先搜索,並將圖G中節點按照DFS訪問順序從1到n編號。DFS建立了一棵以r為根的生成樹T,其節點以先根順序編號。”

第二步:計算半支配點。

發現不管是求semi還是idom,我們都要知道:

找到{P}中semi的dfn最小的點,記為z =min({semi[z]|z>y且存在鏈z->y})。

這兩玩意其實是一個東西,看上去並不好做?

其實這個想想就會啦。

注意到存在z>y的關系,可以考慮按照dfn從大往小搞。

那么在做semi的時候,第一種邊很好搞,第二種邊呢?

因為處理過的點都是z>y的,且在dfs樹中后代結點的dfn總比祖先大。

所以這個時候查詢的x就是對應一條祖先鏈。

操作1:查詢點x的祖先鏈中semi的最小值。

處理完之后我們自然要把x扔進圖中。因為x是當前dfn最小的點,所以它會做某個塊的根。

操作2:給根以父親。

這個用帶權並查集輕松搞定。

(不會帶權並查集的請移步此處QaQ)

第三步:通過半支配點計算支配點。

注意:semi考慮了根而idom時不要,所以我的處理方法是這樣的:

for(id = dfn_num to 2){
  y= (dfn=id的點);
  for( x| (x->y)∈E){
    work_semi(semi[y],x);
  }
  並查集:fa[y]=dfs樹上的fa[y]
  y= (dfn=id-1的點);
  for( x| (semi[x]=y)){
    work_idom(idom[x],y);
  }
}

在(id-1)還沒有被處理的時候把以它為semi的點的itom處理掉就好啦。

 

相關題目

HDU4694

大意:以n為出發點,求每個點支配的點的編號和。

就是個裸的支配樹嘛……

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <vector>
#include <cmath>
#include <map>
#include <set>
#define LL long long
#define FILE "dominator_tree"
using namespace std;

const int N = 200010;
struct Node{int to,next;};
int n,m,dfn[N],clo,rev[N],f[N],semi[N],idom[N],Ans[N];

inline int gi(){
  int x=0,res=1;char ch=getchar();
  while(ch>'9' || ch<'0')res^=ch=='-',ch=getchar();
  while(ch>='0'&&ch<='9')x=x*10+ch-48,ch=getchar();
  return res?x:-x;
}

struct Graph{
  Node E[N];int head[N],tot;
  inline void clear(){
    tot=0;
    for(int i=0;i<=n;++i)head[i]=0;
  }
  inline void link(int u,int v){
    E[++tot]=(Node){v,head[u]};head[u]=tot;
  }
}pre,nxt,dom;

struct uset{
  int fa[N],Mi[N];
  inline void init(){
    for(int i=1;i<=n;++i)
      fa[i]=Mi[i]=semi[i]=i;
  }
  inline int find(int x){
    if(fa[x]==x)return x;
    int fx=fa[x],y=find(fa[x]);
    if(dfn[semi[Mi[fx]]]<dfn[semi[Mi[x]]])Mi[x]=Mi[fx];
    return fa[x]=y;
  }
}uset;

inline void tarjan(int x){
  dfn[x]=++clo;rev[clo]=x;
  for(int e=nxt.head[x];e;e=nxt.E[e].next){
    if(!dfn[nxt.E[e].to])
      f[nxt.E[e].to]=x,tarjan(nxt.E[e].to);
  }
}

inline void dfs(int x,int sum){
  Ans[x]=sum+x;
  for(int e=dom.head[x];e;e=dom.E[e].next)
    dfs(dom.E[e].to,sum+x);
}

inline void calc(){
  for(int i=n;i>=2;--i){
    int y=rev[i],tmp=n;
    for(int e=pre.head[y];e;e=pre.E[e].next){
      int x=pre.E[e].to;if(!dfn[x])continue;
      if(dfn[x]<dfn[y])tmp=min(tmp,dfn[x]);
      else uset.find(x),tmp=min(tmp,dfn[semi[uset.Mi[x]]]);
    }
    semi[y]=rev[tmp];uset.fa[y]=f[y];
    dom.link(semi[y],y);
    
    y=rev[i-1];
    for(int e=dom.head[y];e;e=dom.E[e].next){
      int x=dom.E[e].to;uset.find(x);
      if(semi[uset.Mi[x]]==y)idom[x]=y;
      else idom[x]=uset.Mi[x];
    }
  }

  for(int i=2;i<=n;++i){
    int x=rev[i];
    if(idom[x]!=semi[x])
      idom[x]=idom[idom[x]];
  }
  
  dom.clear();
  for(int i=1;i<n;++i)
    dom.link(idom[i],i);
  dfs(n,0);
  for(int i=1;i<=n;++i){
    printf("%d",Ans[i]),Ans[i]=0;
    i==n?printf("\n"):printf(" ");
  }
}

int main(){
  while(~scanf("%d%d",&n,&m)){
    for(int i=1;i<=m;++i){
      int u=gi(),v=gi();
      nxt.link(u,v);
      pre.link(v,u);
    }
    tarjan(n);
    uset.init();
    calc();
    pre.clear();nxt.clear();dom.clear();
    for(int i=1;i<=n;++i)
      dfn[i]=rev[i]=semi[i]=idom[i]=f[i]=0;
    n=0;m=0;clo=0;
  }
  fclose(stdin);fclose(stdout);
  return 0;
}
HDU4694

 

Codechef GRAPHCNT

大意:問有多少個點對(x,y),滿足存在路徑(1=>x)和(1=>y)且兩條路徑公共點只有1。

就是支配樹上lca為1的點的點對嘛……

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <vector>
#include <cmath>
#include <map>
#include <set>
#define LL long long
#define FILE "graphcnt"
using namespace std;

const int N = 100010;
const int M = 500010;
int n,m,fa[N],dfn[N],rev[N],clo,semi[N],idom[N],size[N];

inline int gi(){
  int x=0,res=1;char ch=getchar();
  while(ch>'9' || ch<'0')res^=ch=='-',ch=getchar();
  while(ch>='0'&&ch<='9')x=x*10+ch-48,ch=getchar();
  return res?x:-x;
}

struct Node{int to,next;};
struct Graph{
  Node E[M];int head[N],tot;
  inline void clr(){
    for(int i=tot=0;i<=n;++i)head[i]=0;
  }
  inline void link(int u,int v){
    E[++tot]=(Node){v,head[u]};
    head[u]=tot;
  }
}pre,nxt,dom;

struct Union_Merge_Set{
  int fa[N],Mi[N];
  inline void init(){
    for(int i=1;i<=n;++i)
      fa[i]=Mi[i]=semi[i]=i;
  }
  inline int find(int x){
    if(fa[x]==x)return x;
    int fx=fa[x],y=find(fa[x]);
    if(dfn[semi[Mi[fx]]]<dfn[semi[Mi[x]]])Mi[x]=Mi[fx];
    return fa[x]=y;
  }
}uset;

inline void tarjan(int x){
  dfn[x]=++clo;rev[clo]=x;
  for(int e=nxt.head[x];e;e=nxt.E[e].next)
    if(!dfn[nxt.E[e].to])
      fa[nxt.E[e].to]=x,tarjan(nxt.E[e].to);
}

inline void build(){
  for(int i=n;i>=2;--i){
    int y=rev[i],tmp=n;if(!y)continue;
    for(int e=pre.head[y];e;e=pre.E[e].next){
      int x=pre.E[e].to;if(!dfn[x])continue;
      if(dfn[x]<dfn[y])tmp=min(tmp,dfn[x]);
      else uset.find(x),tmp=min(tmp,dfn[semi[uset.Mi[x]]]);
    }
    semi[y]=rev[tmp];uset.fa[y]=fa[y];
    dom.link(semi[y],y);

    y=rev[i-1];if(!y)continue;
    for(int e=dom.head[y];e;e=dom.E[e].next){
      int x=dom.E[e].to;uset.find(x);
      if(semi[uset.Mi[x]]==y)idom[x]=y;
      else idom[x]=uset.Mi[x];
    }
  }
  for(int i=2;i<=n;++i){
    int x=rev[i];
    if(idom[x]!=semi[x])
      idom[x]=idom[idom[x]];
  }
  dom.clr();
  
  for(int i=2;i<=n;++i)
    dom.link(idom[rev[i]],rev[i]);
}

inline void dfs(int x){
  size[x]=1;
  for(int e=dom.head[x];e;e=dom.E[e].next){
    int y=dom.E[e].to;if(size[y])continue;
    dfs(y);size[x]+=size[y];
  }
}

inline LL calc(LL Ans=0,LL sum=0){
  for(int e=dom.head[1];e;e=dom.E[e].next){
    int y=dom.E[e].to;
    Ans+=sum*size[y];
    sum+=size[y];
  }
  return Ans+size[1]-1;
}

int main(){
  n=gi();m=gi();
  for(int i=1;i<=m;++i){
    int u=gi(),v=gi();
    nxt.link(u,v);
    pre.link(v,u);
  }
  tarjan(1);
  uset.init();
  build();
  dfs(1);
  printf("%lld",calc());
  return 0;
}
Codechef GRAPHCNT

 

 

最后來波總結

算法時間復雜度O(nα(n)),空間復雜度O(n),但是常數比較大,雖然跑得還是很快。

支配樹本身的代碼還是比較短的,細節有一點但都很正常,只要理解了絕對沒有什么問題,就算沒理解也沒什么問題……

這方面的題目目前比較少?小強和阿米巴?畢竟2014年才在Wc中普及……可能就快了吧,畢竟還是有一定實際意義和證明難度的。

說起Wc又是另一回事了……

怎么年年Wc扯支配樹啊......


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM