算法 {Tarjan算法,强连通分量SCC,BCC双连通分量,割边/桥}

算法 {Tarjan求强连通分量SCC}
@LOC: 3

有向图-SCC強聯通分量

定義

#SCC強聯通分量Strongly Connected Component#
有向圖G的所有點 劃分為若干個不相交的子集S1,S2,..., Si對應的導出子圖是強聯通的;
G的所有邊 分為2類: {1:[在Si的導出子圖裡]; 2:[形如Si->Sj]};

相關定義

#連通子圖#
有向圖G, 對於其子圖SG 如果他是連通的(這裡的連通 是泛型的 你可以定義為{強/半/弱})(即此時單獨考慮SG 與G無關) 則SG為G的連通子圖;

#連通分量CCConnected Component(連通塊)#
極大的連通子圖(這裡的連通 是泛型的 你可以定義為{強/半/弱}) 為連通分量; 有向圖G的所有連通子圖 在包含關係下為偏序關係, 其所有極大元 為G的所有連通分量;
連通分量 一定是導出子圖, 因為對於連通分量CC 增加其內部的邊 不會影響CC的連通性; 因此 連通分量可以直接用(點集)來表示;

舉例
G: (a->b->c) (d->e), a->b為連通子圖, a->b->c是連通分量(當然也是連通子圖);

@DELI;

#強聯通SCStrongly Connceted#
對於有向圖G, 任意兩個點a,b是強連通的    ⟺    \iff (a可達b) 且 (b也可達a);

@DELI;

#有向圖是強連通的#
有向圖G, 如果(G的任意兩個點 都是強連通的)    ⟹    \implies G是強連通的;

@DELI;

#有向圖中兩點間的連通性#
對於有向圖G, 任意兩個點a,b是連通的    ⟺    \iff (a可達b) 或 (b可達a);

@DELI;

#SCC的縮點#
令有向圖DAG滿足: 原圖G中的任意邊a->b, 如果Scc_id[a] != Scc_id[b], 則他對應在圖DAG中為Scc_id[a] -> Scc_id[b]的一條有向邊;
即 可以想象為: 原圖G中的一個SCC(他由很多點組成) 縮成了一個DAG中的一個點;

性質

@DELI;

對於一個強聯通的有向圖G, G裡面的兩個點a,b 其滿足: 刪除原圖G中的任何一條有向邊後 a,b仍然是連通的(只不過 可能不是強連通了, 比如原來是a可達b && b可達a 而現在變成了a可達b 但b不可達a);

算法

模板代碼

namespace ___Tarjan_SCC{
vector<int> SCC_id;   // `SCC_id[a]=b`: 圖中的`a`點所在的SCC(強聯通塊)的ID號為`b`(滿足`0<=b<SCC_count`);
vector<int> SCC_size; // `SCC_size[a]=b`: ID號為`a`的SCC 他由`b`個點組成; `SCC_size.size()`為SCC的個數;
stack<int> __Stk;
vector<int> __DfsId, __LowId;
int __DfsIdCounter;
const ___Graph * __DiG; // 要處理的*有向圖*;

void __Dfs( int _cur){
    __Stk.push(_cur);  __DfsId[ _cur] = __DfsIdCounter ++;  __LowId[_cur] = __DfsId[_cur];
    for( int nex, edge = __DiG->Head[ _cur]; ~edge; edge = __DiG->Next[ edge]){
        nex = __DiG->Vertex[ edge];
        if( -1 == __DfsId[ nex]){ __Dfs( nex);}
        if( -1 == SCC_id[ nex]){ MinSelf_( __LowId[ _cur], __LowId[ nex]);}
    }
    if( __DfsId[ _cur] == __LowId[ _cur]){
        SCC_size.push_back( 0);
        while( true){
            int temp = __Stk.top();  __Stk.pop();
            SCC_id[ temp] = SCC_size.size() - 1;  SCC_size.back() ++;
            if( temp == _cur){ break;}
        }
    }
}
void Work( const ___Graph & _DiG){
    __DiG = &_DiG;
    SCC_id.resize( __DiG->PointsCount);  memset( SCC_id.data(), -1, sizeof( SCC_id[0]) * SCC_id.size());  SCC_size.clear();
    while( false == __Stk.empty()){ __Stk.pop();}
    __DfsId.resize( __DiG->PointsCount);  __LowId.resize( __DiG->PointsCount);  memset( __DfsId.data(), -1, sizeof( __DfsId[0]) * __DfsId.size());
    __DfsIdCounter = 0;
    for( int i = 0; i < __DiG->PointsCount; ++i){
        if( -1 == __DfsId[ i]){ __Dfs( i);}
    }
}
void Build_DAG( ___Graph & _dag){
//< *SCC的縮點*;
    _dag.Initialize( SCC_size.size());
    for( int cur = 0; cur < __DiG->PointsCount; ++cur){
        for( int nex, edge = __DiG->Head[cur]; ~edge; edge = __DiG->Next[edge]){
            nex = __DiG->Vertex[ edge];
            if( SCC_id[ cur] == SCC_id[ nex]){ continue;}
            _dag.Add_edge( SCC_id[ cur], SCC_id[ nex], __DiG->Weight[ edge]);
        }
    }
}
} // namespace ___Tarjan_SCC
注釋

SCC算法 依靠的核心是: 一個SCC(點集) 在DFS樹中 是一個子樹(這裡不是說的以x為根的子樹, 而是子樹 他的定義是子樹是一個點集S, S中任何兩點 在原樹中的路徑上的所有點 都在S中), 因為子樹 一定只有1個(樹根) 也就是最小時間戳; 即當DFS回溯到這個(子樹的樹根)時 此時stack裡頂層的連續一段存儲的 就是這個子樹 即這個割集 即stack形如[bottom..., SCC];

@DELI;

LowIdTarjan算法的核心;
他的定義: 對於一個SCC 令其最小的DfsId[st]mi, 則我們的目的是 對任意 x ∈ S C C x \in SCC xSCC
. 1(x==st):[LowId[st]=mi];
. 2(x!=st):[LowId[x] != DfsId[t], 且t \in SCC, DfsId[t] < DfsId[x]]; 換句話說, 此時x的LowId值 等於該SCC內某個點t的時間戳 且滿足t的時間戳是小於DfsId[x]的;
也就是, 只要不是在st這個點 那麼他的LowId一定不等於他的DfsId (即這個點 仍然會留在stack裡面), 只有當在st這個點時 才會有LowId = DfsId (然後此時stack=[... st, others]);
比如0<->1<->2 (先遍歷1->2然後才是1->0), 最終會得到Low[0,1] = 0, Low[2]=1;
. 因此 代碼中的MinSelf_( __LowId[ _cur], __LowId[ nex]); 你寫成DfsId[nex]也可以, 你寫成LowId[nex] 並不能保證 一個SCC裡的所有點的LowId值都相同, 比如上面這個樣例;

@DELI;

對於當前的Dfs(cur)中的一條邊cur->nex 有如下幾種情況:

for( int nex, edge = __DiG->Head[ _cur]; ~edge; edge = __DiG->Next[ edge]){
    nex = __DiG->Vertex[ edge];
    if( -1 == __DfsId[ nex]){ 
	    __Dfs( nex);
	    if( SCC_id[nex] != -1): *第1類邊*;
	    else: *第2類邊*;
    }
    else{
    	if( `nex,cur`不是同一個*DFS樹*上): *第3類邊*;
    	elif( `SCC_id[nex] != -1`): *第4類邊*;
    	else: *第5類邊*;
	}
}

`SCC_id[x]!=-1`等價於`IsInStk[x]=false`;

1(DfsId[nex] == -1): 即此時要進行Dfs(nex), 即這條邊是DFS樹上邊, 此時分2種情況:
. 第1類邊; @IF(SCC_id[nex] != -1)):[ cur,nex不在同一個SCC裡, 當前邊是橫跨2個SCC的];
. 第2類邊;@ELSE:[cur,nex在同一個SCC裡, 此時要執行MinSelf( LowId[cur], LowId[nex]); 比如0->1->2->0, Dfs(2)之後此時Low[2]=0 回溯到Dfs(1)時 更新Low[1]=0;];
2(DfsId[nex] != -1): 此時 即Dfs(nex)先前已經調用過了 此時分為3種情況:
. 第3類邊;@IF(nex不是當前DFS樹裡的節點):[ 比如0, 1->0 第一次的DFS樹是0 第二次的DFS樹是1, 此時訪問到(先前的DFS樹)上的節點 而先前的DFS樹 他的SCC都已經確定了; 因此 cur,nex不在同一個SCC裡, 當前邊是橫跨2個SCC的];
. 第4類邊; @ELIF(SCC_id[nex]!=-1):[ nex是當前DFS樹的節點 但是他所在的SCC已經確定; 比如0->1, 0->2->1 對於Dfs(2)時的2->1這條邊 此時1所在的SCC{1}已經確定; 跟上面@IF的情況一樣 cur|nex不會在同一個SCC裡];
. 第5類邊; @ELSE:[SCC_id[nex]==-1):[ nex是當前DFS樹的節點 且此時nex是DFS樹上的cur祖先節點, 即存在一條nex->...->cur的路徑(也就是當前的DFS棧是形如[...nex,...,cur]) 然後當前又出現了一條cur->nex的路徑, 此時形成了一個環,因此cur,nex這個環 一定是強聯通的 因此更新MinSelf( LowId[cur], LowId[nex]);]

性質

例题

@LINK: https://blog.csdn.net/qq_66485519/article/details/128474214;

筆記

For a (directed) graph G = ( V , E ) G = (V,E) G=(V,E), let I d [ v ] Id[v] Id[v] is the SCC-ID of v ∈ V v \in V vV which within the range [0, SCC-Count).

A DFS-Order-Sequence of a graph is [ 0 , 1 , 2 , . . . , n ) [0, 1, 2, ..., n) [0,1,2,...,n), we use O r d e r [ v ] Order[v] Order[v] represents its DFS-Order.

Let L o w [ v ] Low[v] Low[v] denotes a DFS-Order such that L o w [ v ] = O r d e r [ w ] Low[v] = Order[w] Low[v]=Order[w] such that O r d e r [ w ] ≤ O r d e r [ v ] Order[w] \leq Order[v] Order[w]Order[v] and w , v w,v w,v are in the same SCC
. If there exists a point w w w such that O r d e r [ w ] < O r d e r [ v ] Order[w] < Order[v] Order[w]<Order[v] and w , v w,v w,v in the same SCC, then L o w [ v ] < O r d e r [ v ] Low[v] < Order[v] Low[v]<Order[v]; otherwise L o w [ v ] = O r d e r [ v ] Low[v] = Order[v] Low[v]=Order[v] if and only if O r d e r [ v ] Order[v] Order[v] is the minimal-Order in its SCC (every SCC has exactly one such point, we call such a point The Root of SCC).

+ e.g., G = ( 1 → 2 ) ( 2 → 3 ) ( 3 → 4 ) ( 4 → 3 ) ( 2 → 5 ) ( 5 → 2 ) ( 2 → 1 ) G = (1\to 2)(2\to 3)(3\to 4)(4\to 3)(2\to 5)(5\to 2)(2\to 1) G=(12)(23)(34)(43)(25)(52)(21) (note the order of edges is also the process of DFS(1)), then we have 2 2 2 SCC ( 1 , 2 , 5 ) ( 3 , 4 ) (1,2,5) (3,4) (1,2,5)(3,4); finally, L o w [ 1 ] = 0 , L o w [ 2 ] = 0 , L o w [ 5 ] = 1 Low[1] = 0, Low[2] = 0, Low[5]=1 Low[1]=0,Low[2]=0,Low[5]=1, all L o w [ v ] Low[v] Low[v] in a SCC maybe not the same; L o w [ 2 ] = 0 Low[2] = 0 Low[2]=0 cuz there is O r d e r [ 1 ] = 0 < O r d e r [ 2 ] = 1 Order[1] = 0 < Order[2]=1 Order[1]=0<Order[2]=1; L o w [ 5 ] = 1 Low[5] = 1 Low[5]=1 cuz there is O r d e r [ 2 ] = 1 < O r d e r [ 5 ] = 4 Order[2] = 1 < Order[5]=4 Order[2]=1<Order[5]=4 ( L o w [ 5 ] Low[5] Low[5] can also equals O r d e r [ 1 ] = 0 Order[1] = 0 Order[1]=0); 0 0 0 is the root of the SCC 1 , 2 , 5 1,2,5 1,2,5)


When we at D f s ( c u r ) Dfs( cur) Dfs(cur) and iterating its adjacent-points n e x nex nex, there are several steps:
+ Firstly, we set Order[cur] = Low[cur] = DFS_order ++ (cuz Low[cur] <= Order[cur] finally, so we set it equals to Order[cur] initially)
+ If nex has never been called by DFS, so we call D f s ( n e x ) Dfs(nex) Dfs(nex); and then we update L o w [ c u r ] = m i n ( s e l f , L o w [ n e x ] ) Low[cur] = min(self, Low[nex]) Low[cur]=min(self,Low[nex]) (cuz nex never be DFS visited, if c u r , n e x cur, nex cur,nex is not in the same SCC and O r d e r [ n e x ] > O r d e r [ c u r ] Order[nex] > Order[cur] Order[nex]>Order[cur], we have n e x nex nex must the Root of its SCC, so L o w [ n e x ] > O r d e r [ c u r ] Low[nex] > Order[cur] Low[nex]>Order[cur]; this operation is ineffective), otherwise c u r , n e x cur,nex cur,nex in the same SCC, if c u r cur cur is the Root then L o w [ n e x ] = O r d e r [ c u r ] Low[nex] = Order[cur] Low[nex]=Order[cur], so this operations is also ineffective; if c u r cur cur is not the Root, there must exists a point n e x 1 nex1 nex1 whose L o w [ n e x 1 ] < O r d e r [ c u r ] Low[nex1] < Order[cur] Low[nex1]<Order[cur] (cuz there must has a path c u r → n e x 1 → . . → r o o t cur \to nex1 \to .. \to root curnex1..root where . . . ... ... do not contains c u r cur cur, therefore L o w [ n e x 1 ] = O r d e r [ r o o t ] < O r d e r [ c u r ] Low[nex1] = Order[root] < Order[cur] Low[nex1]=Order[root]<Order[cur]);

+ When DFS returns to a root-point Low[] = Order[], then the DFS-Stack must be in the form [...., root, S] where any point that in the same SCC with root, must occurs in S;

Code

void dfs( int _cur){
    queue[ tail ++] = _cur;
    isInQueue[ _cur] = true;
    dfsOrderId[ _cur] = dfsOrderId_counter ++;
    lowest_orderId[ _cur] = dfsOrderId[ _cur];
    //--
    for( int nex, i = graph->Head[ _cur]; ~i; i = graph->Next[ i]){
        nex = graph->Vertex[ i];
        if( -1 == dfsOrderId[ nex]){ //< never `dfs( nex)` called
            dfs( nex);
        }
        if( isInQueue[ nex]){ //< `nex` maybe already forms a SCC, we should aviod this case; e.g., (1<->2)<-3, so Low[2] cann't be used to updated Low[3]
            lowest_orderId[ _cur] = min( lowest_orderId[ nex], lowest_orderId[ _cur]);
        }
    }
    if( lowest_orderId[ _cur] == dfsOrderId[ _cur]){
        scc_size[ scc_id_counter] = 0;
        while( true){
            int c = queue[ tail - 1];
            isInQueue[ c] = false;
            -- tail;
            scc_id[ c] = scc_id_counter;
            ++ scc_size[ scc_id_counter];
            if( c == _cur){ break;}
        }
        ++ scc_id_counter;
    }
}

Actually, the array dfsOrderId, lowest_orderId can be omitted; (use scc_id to denote lowest_orderId, cuz when DFS not yet returned to root, the array scc_id is free; and once a SCC is fixed (i.e., DFS has returned to its root, any other points (not belongs to this SCC) would not visit/use any information of this SCC)

 void dfs( int _cur){
    ptrNew_queue[ queue_tail ++] = _cur;
    ptrNew_isInQueue[ _cur] = true;
    int current_dfsOrderId = dfsOrderId_counter;
    ptrNew_sccId[ _cur] = dfsOrderId_counter ++;
    //--
    for( int nex, i = ptrRef_graph->Head[ _cur]; ~i; i = ptrRef_graph->Next[ i]){
        nex = ptrRef_graph->Vertex[ i];
        if( -1 == ptrNew_sccId[ nex]){ //< never `dfs( nex)` called
            dfs( nex);
        }
        if( ptrNew_isInQueue[ nex]){
            ptrNew_sccId[ _cur] = min( ptrNew_sccId[ nex], ptrNew_sccId[ _cur]);
        }
    }
    if( ptrNew_sccId[ _cur] == current_dfsOrderId){
        ptrNew_sccSize[ scc_id_counter] = 0;
        while( true){
            int c = ptrNew_queue[ queue_tail - 1];
            ptrNew_isInQueue[ c] = false;
            -- queue_tail;
            ptrNew_sccId[ c] = scc_id_counter;
            ++ ptrNew_sccSize[ scc_id_counter];
            if( c == _cur){ break;}
        }
        ++ scc_id_counter;
    }
}
  • 25
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值