[Tarjan系列] Tarjan算法與有向圖的SCC


前面的文章介紹了如何用Tarjan算法計算無向圖中的e-DCC和v-DCC以及如何縮點。

本篇文章資料參考:李煜東《算法競賽進階指南》

這一篇我們講如何用Tarjan算法求有向圖的SCC( 強連通分量 )已經如何縮點。

給定一張有向圖,若對於圖中任意兩個節點x和y,

既有x到y的路徑,又有y到x的路徑,則該有向圖是一張“強連通圖”。

有向圖的極大連通子圖被稱為“強連通分量”,即SCC。

一個環一定是強連通圖。如果既有x到y的路徑,又有y到x的路徑,那么x和y就一定在一個環中。

這就是Tarjan算法的原理:對於每個點x,找到與它一起能構成環的所有點。

下面介紹有向圖中的三種邊(x,y):

1. 樹枝邊:搜索樹中x是y的父節點

2. 前向邊:搜索樹中x是y的祖先節點

3. 后向邊:搜索樹中y是x的祖先節點

4. 橫叉邊:除了以上三種情況外的邊,滿足dfn[y]<dfn[x]

這里只給出簡單定義,不再贅述。

我們可以發現,用Tarjan算法求SCC時,后向邊(x,y)可以和搜索樹上從y到x的路徑構成一個環。

除后向邊外,通過橫叉邊也可能找到一條從y出發能回到x的祖先節點的路徑。

那么為了找到通過橫叉邊和后向邊構成的環,Tarjan算法在dfs的過程中維護一個棧,當訪問到節點x時,棧中需要保存以下兩類節點:

1. 搜索樹上x的祖先節點,記為集合anc(x)。設y∈anc(x),若存在一條后向邊(x,y),則(x,y)和y到x之間的路徑一起形成環。

2. 已經訪問過,並且存在一條路徑到達anc(x)的節點。

設z時一個這樣的點,從z出發存在一條路徑到達y∈anc(x)。若存在橫叉邊(x,y),則(x,z)、z到y的路徑、y到x的路徑形成一個環。

綜上,棧中的節點就是能從x出發點的“后向邊”和“橫叉邊”形成環的節點。

至此,我們引入追溯值low[x]的概念,有向圖的Tarjan算法里面的定義和無向圖是不一樣的。

還是設subtree(x)表示以x為根的子樹。x的追溯值low[x]定義為滿足一下條件的節點的最小dfn:

1. 該點在棧中 2. 存在一條從subtree(x)出發的有向邊,以該點為終點

根據以上定義,Tarjan算法根據以下步驟計算low[x]:

1. 當節點x第一次被訪問時,將x入棧,初始化low[x]=dfn[x]

2. 掃描從頭x出發的每條邊(x,y),若y沒被訪問過,則說明(x,y)時樹枝邊,遞歸訪問y,從y回溯之后,令low[x]=min(low[x],low[y]),若y被訪問過且y在棧中,令low[y]=min(low[x],dfn[y])

3. 從x回溯之前,判斷是否有low[x]=dfn[x],若成立,則不斷從棧中彈出節點直至x出棧。

SCC的判定法則:

在上面的計算步驟3中,從棧中從x到棧頂的所有節點構成一個SCC。

少廢話,上代碼!

好der~

#include<bits/stdc++.h>
#define N 1000010
using namespace std;
inline int read(){
    int data=0,w=1;char ch=0;
    while(ch!='-' && (ch<'0'||ch>'9'))ch=getchar();
    if(ch=='-')w=-1,ch=getchar();
    while(ch>='0' && ch<='9')data=data*10+ch-'0',ch=getchar();
    return data*w;
}
struct Edge{
    int nxt,to;
    #define nxt(x) e[x].nxt
    #define to(x) e[x].to
}e[N<<1];
int head[N],tot=1;
inline void addedge(int f,int t){
    nxt(++tot)=head[f];to(tot)=t;head[f]=tot;
}
int dfn[N],low[N],stk[N],ins[N],c[N];
vector<int> scc[N];
int n,m,cnt,top,num;
void tarjan(int x){
    dfn[x]=low[x]=++cnt;
    stk[++top]=x,ins[x]=1;
    for(int i=head[x];i;i=nxt(i)){
        int y=to(i);
        if(!dfn[y]){
            tarjan(y);
            low[x]=min(low[x],low[y]);//搜索樹上的點
        }else if(ins[y])
            low[x]=min(low[x],dfn[y]);//y在棧中且y被訪問過了
   }
  if(dfn[x]==low[x]){ num++;int z;//新的一個SCC do{ z=stk[top--],ins[z]=0;//彈出棧頂元素z c[z]=num,scc[num].push_back(z);//z插入存第num個SCC的vector里 }while(z!=x);//直到x被彈出棧 } } int main(){ n=read();m=read(); for(int i=1;i<=m;i++){ int x=read(),y=read(); addedge(x,y); } for(int i=1;i<=n;i++) if(!dfn[i])tarjan(i); for(int i=1;i<=num;i++){ printf("%d:",i); for(int j=0;j<scc[i].size();j++){ printf(" %d",scc[i][j]); } putchar(10); } return 0; }

SCC的縮點就非常簡單了,上面我們已經用c[x]儲存了每個點所在的SCC的編號,那我們直接類似e-DCC的縮點,把每個SCC縮成一個點,若c[x]≠c[y],我們就在編號為c[x]和c[y]的SCC中連一條邊就可以得到一個有向無環圖( DAG )。

代碼真的非常簡單,甚至不需要再跑一遍dfs。

給出代碼:

for(int x=1;x<=n;x++)
    for(int i=head[x];i;i=nxt(i)){
        int y=to(i);
        if(c[x]==c[y])continue;
        addedge_c(c[x],c[y]);
        }
//夠簡單了吧...

整個程序的代碼我就不貼出來了,建新圖和我前面e-DCC縮點的博客完全一致。

下一篇講點數學,別忘了來聽課。


免責聲明!

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



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