tarjan算法求強連通份量的應用:有向圖縮環爲點

tarjan提出了不少算法.本文討論的是圖論中求解強連通份量的那個tarjan算法...的應用。c++

講得不會很是基礎,甚至只是起到記錄知識的做用.算法

建議先閱讀他人的文章,在對tarjan算法有了大概瞭解後再繼續讀下去.spa

本文討論的核心是.net

  • 有向圖爲什麼要縮點code

  • 什麼是有向圖縮點blog

  • 有向圖縮點的實現細節圖片

  • hihoCoder-1185 的實現代碼ci


本文暫時(也多是永久)不涉及get

  • tarjan算法的正確性證實it

  • tarjan算法的理解

引例

例題:hihoCoder-1185

考慮一個有向圖,起始點爲1,每一個點用正整數編號.給出連通關係以及各節點權值.權值都是天然數.

問: 從1點出發,終點隨意,最大路上節點權和能夠是多少?

例如: 1->2 就是個合法路徑. 這個路徑的權值和是6.

如[1]所示.方括號內的數字是此節點的權值.

有向圖的環,圖片來自hihoCoder 1185

先考慮暴力算法,DFS.但直接搜下去會死循環.

爲何呢?這是由於這個有向圖中有環.

3->66->3這兩條邊使得3和6處在一個環內.兩個點強連通.

若是不加處理地從1點開始DFS,一定會在3和6之間來回搜索,由於這樣路上的節點權和就會無限增加.

有向圖爲什麼要縮點

那麼,把全部的"環"都收縮爲一個點,那就成了有向無環圖(DAG)了.該DP就DP,該DFS就 DFS,毫無困擾了.

什麼是有向圖縮點

縮點以後是這樣的:

縮點,圖片來自hihoCoder 1185

這就把6號點"合併"進了3號點.合併以後,3號點和6號點之後等價.

爲了方便,咱們用3號點來"表明"6號點和環內其餘的點,若是有的話.

這個思想和並查集中"表明元"的思想很像.而且咱們約定:編號爲n的節點的"表明元"是contract[n].若是節點n不在環中,爲了通常性,令contract[n] = n,即本身的表明元爲本身.這與並查集的約定相同.

有向圖縮點的實現細節

將一堆點"合併"爲一個"表明元"要修改的有兩個東西.一個是邊,一個是權.

修改邊和權,具體實現起來有兩種風格.

  1. 修改現成的圖

  2. 從新建圖

筆者喜歡修改圖.從新建圖是弱者的行爲(笑).

首先,用tarjan算法找出從1點出發能到達的全部環...固然每次只會找出一個環.咱們說過,每一個環都有且僅有一個"表明元".

接着對於這個環上的每一個非表明元節點,

  1. 把它的全部出邊複製給"表明元"後刪除.

  2. 把它的權值加到表明元上.

你可能會想到: 出邊所有刪除了,那"其餘節點"進入環中非表明元節點的邊怎麼辦呢?換言之,非表明元節點的入邊怎麼解決?

很簡單,對於圖中的每條入邊指向的節點編號k,都令其等於contract[k].
正所謂"有則改之無則加勉"(逃

hihoCoder-1185 的實現代碼

//AC, 116ms
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 2e4 + 100;
vector<int> e[maxn];
int ins[maxn], dfn[maxn], low[maxn], contract[maxn];
ll w[maxn];
int ind;
stack<int> s;
void tarjan(int u) {
    dfn[u] = low[u] = ++ind;
    ins[u] = 1;
    s.push(u);
    for(int i = 0; i < e[u].size(); i++) {
        int v = e[u][i];
        if(!dfn[v]) {
            tarjan(v);
            low[u] = min(low[u], low[v]);
        } else if(ins[v]) low[u] = min(low[u], dfn[v]);
    }
    if(dfn[u] == low[u]) {
        int v;
        do {
            v = s.top();
            s.pop();
            ins[v] = 0;
            contract[v] = u;
            if(u != v) {
                w[u] += w[v];
                while(!e[v].empty()) {
                    e[u].push_back(e[v].back());
                    e[v].pop_back();
                }
            }
        } while(u != v);
    }
}

ll dfs(int u, ll cnt) {
    cnt += w[u];
    ll re = cnt;
    for(int i = 0; i < e[u].size(); i++) {
        int v = contract[e[u][i]];
        if(v != u) re = max(re, dfs(v, cnt));
    }
    return re;
}
int main() {
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i++) {
        cin >> w[i];
    }
    for(int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        e[u].push_back(v);
    }
    tarjan(1);
    cout << dfs(1, 0) << endl;
    return 0;
}

由於這份代碼用的是vector<int>[]的鄰接表存邊法,因此效率並非十分高.

各類細節一如前文所述.

之後有空再加一份用鏈式前向星(咱們一般叫作鏈表)作鄰接表的代碼.

相關文章
相關標籤/搜索