強連通份量

有向圖的強連通份量

基本概念

連通份量:

對於份量內任意兩點\(u 和 v\) , 必然能夠找到從 \(u\) 走到 \(v\) 且能夠從 \(v\) 走到 \(u\).ios

強連通份量:

極大連通份量(包含點數最多)算法

強連通份量經常使用於縮點數組

Tarjan算法:

基於 \(DFS\) :網絡

Tarjan算法幾個重要概念:

在已經\(DFS\)的樹中:ide

  1. 後前邊: (x, y) x是y的一個祖先, 但存在一條由y->x的邊.
  2. 橫插邊: (x, y) x和y不屬於同一條分支, 但存在一條y->x的邊
  3. 前向邊: (x, y) y是x的祖先, 存在一條x->y的邊

幾個數組和變量:

  1. 時間戳: 記錄搜索到每一個點的時間.即對每一個點根據搜索順序進行標號排序.
  2. dfn數組: 記錄每一個點的時間戳.(同時具備判重數組做用)
  3. low數組: 記錄每一個點向上走所能達到的最高點(即時間戳最小點).
  4. stk棧: 記錄當前強連通份量內的點.
  5. id數組: 記錄每一個點所在的連通份量.
  6. scc_cnt: 強連通份量個數

模板:

int timecnt;            //時間戳
int dfn[N];             //每一個點的時間戳
int low[N];             //low[u] : u所在的子樹中全部點中所能向上走到的時間戳最小的點
int scc_cnt;            //強連通份量的數量
int id[N];              //id[i] : 表示i號點所在的強連通份量的編號
stack<int> stk;         //存儲當前強連通份量裏的全部點
int in_stk[N];          //記錄該點是否在棧中

void tarjan(int u)
{
    low[u] = dfn[u] = ++ timecnt;
    stk.push(u);
    in_stk[u] = 1;
    
    for (int i = h[u]; i != -1; i = ne[i]){
        int j = e[i];
        
        if (!dfn[j]){               //j點沒有被遍歷過,j點必定是在子樹中

            tarjan(j);              //遍歷j
            low[u] = min(low[u], low[j]);       //遍歷事後的j的low可能已經找到一個更高的結點,因此要去更新u
            
        }
        else if (in_stk[j])         //j在棧中,則j和u之間必定是一條橫叉邊或向前邊,即j的時間戳必定比u小
            low[u] = min(low[u], dfn[j]);
    }
    
    if (low[u] == dfn[u]){          //到此處,u的全部邊已經遍歷完,若是low[u] = dfn[u] : 獲得了一個強連通份量
        scc_cnt ++;
        int y;                      //此時該強連通份量裏的點全在棧中,所有取出
        do{
            y = stk.top();
            stk.pop();
            id[y] = scc_cnt;
            sizes[scc_cnt] ++;
        }while (y != u);
    }
    
}
例題

AcWing 1174. 受歡迎的牛

算法思路 :

強連通份量的典型應用:縮點. 將整個圖縮點後,獲得一張拓撲圖, 求出強連通份量後, 尋找出度爲0的節點, 該節點內的牛的數量爲答案(注意出度爲0的點只能有一個,不然結果爲0)spa

#include <iostream>
#include <cstring>
#include <queue>
#include <stack>

using namespace std;

const int N = 1e5 + 10, M = 5e4 + 10;

int h[N], e[M], ne[M], idx;

int timecnt;            //時間戳
int dfn[N];             //每一個點的時間戳
int low[N];             //low[u] : u所在的子樹中全部點中所能向上走到的時間戳最小的點
int scc_cnt;            //強連通份量的數量
int id[N];              //id[i] : 表示i號點所在的強連通份量的編號
stack<int> stk;         //存儲當前強連通份量裏的全部點
int in_stk[N];          //記錄該點是否在棧中

int sizes[N];            //強連通份量的結點數量
int dout[N];            //每一個強連通份量的出度
int n, m;

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

void tarjan(int u)
{
    low[u] = dfn[u] = ++ timecnt;
    stk.push(u);
    in_stk[u] = 1;
    
    for (int i = h[u]; i != -1; i = ne[i]){
        int j = e[i];
        
        if (!dfn[j]){               //j點沒有被遍歷過,j點必定是在子樹中

            tarjan(j);              //遍歷j
            low[u] = min(low[u], low[j]);       //遍歷事後的j的low可能已經找到一個更高的結點,因此要去更新u
            
        }
        else if (in_stk[j])         //j在棧中,則j和u之間必定是一條橫叉邊或向前邊,即j的時間戳必定比u小
            low[u] = min(low[u], dfn[j]);
    }
    
    if (low[u] == dfn[u]){          //到此處,u的全部邊已經遍歷完,若是low[u] = dfn[u] : 獲得了一個強連通份量
        scc_cnt ++;
        int y;                      //此時該強連通份量裏的點全在棧中,所有取出
        do{
            y = stk.top();
            stk.pop();
            id[y] = scc_cnt;
            sizes[scc_cnt] ++;
        }while (y != u);
    }
    
} 


int main()
{
    cin >> n >> m;
    
    memset (h, -1, sizeof h);
    
    for (int i = 1; i <= m; i ++ ){
        int a, b;
        cin >> a >> b;
        add(a, b);
    }
    
    for (int i = 1; i <= n; i ++ )
        if (!dfn[i])
            tarjan(i);
    
    for (int i = 1; i <= n; i ++ )
        for (int j = h[i]; j != -1; j = ne[j]){
            int k = e[j];
            
            int a = id[i], b = id[k];
            if (a != b){                //兩者不在同一個強連通份量內,則由i -> k的邊是縮點後點一條邊,對於連通份量: a -> b
                dout[a] ++;
            }
        }
        
    int cnt = 0;
    int sum = 0;
    for (int i = 1; i <= scc_cnt; i ++ )
        if (!dout[i]){
            cnt ++;
            sum = sizes[i];
            if (cnt > 1){
                sum = 0;
                break;
            }
        }
        
    cout << sum << endl;
    
    return 0;
}

AcWing 367. 學校網絡

算法思路:

強連通份量縮點, 將原圖轉化爲拓撲圖, 第一問求入度爲0的點的個數, 第二問結論: $ max(cnt_{in}, cnt_{out})$排序

#include <iostream>
#include <cstring>
#include <algorithm>
#include <stack>
#include <queue>

using namespace std;

const int N = 110, M = N * N / 2;

int h[N], e[M], ne[M], idx;

int timecnt;
int low[N], dfn[N];
stack<int> stk;
int scc_cnt;
int id[N];
int in_stk[N];
int dout[N];
int din[N];

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

void tarjan(int u)
{
    dfn[u] = low[u] = ++timecnt;
    stk.push(u);
    in_stk[u] = 1;
    
    for (int i = h[u]; i != -1; i = ne[i]){
        int j = e[i];
        
        if (!dfn[j]){
            tarjan(j);
            low[u] = min(low[u], low[j]);
        }else if (in_stk[j])
            low[u] = min(low[u], dfn[j]);
    }
    
    if (dfn[u] == low[u]){
        scc_cnt ++;
        int y;
        do{
            y = stk.top();
            stk.pop();
            in_stk[y] = 0;
            id[y] = scc_cnt;
            
        }while (y != u);
    }
    
}

int main()
{
    int n;
    cin >> n;
    memset (h, -1, sizeof h);
    for (int i = 1; i <= n; i ++ ){
        int b;
        while (cin >> b && b){
            add(i, b);
        }
    }
    
    for (int i = 1; i <= n; i ++ )
        if (!dfn[i])
            tarjan(i);
    
    for (int i = 1; i <= n; i ++ )
        for (int j = h[i]; j != -1; j = ne[j]){
            int k = e[j];
            int a = id[i], b = id[k];
            if (a != b){
                dout[a] ++;
                din[b] ++;
            }
        }
    
    int in_cnt = 0;
    int out_cnt = 0;
    for (int i = 1; i <= scc_cnt; i ++ ){
        if (!dout[i]){
            out_cnt ++;
        }
        if (!din[i]){
            in_cnt ++;
        }
    }
    
    if (scc_cnt == 1)
        cout << 1 << endl <<  0 << endl;
    else
        cout << in_cnt << endl << max(in_cnt, out_cnt) << endl;
    
    return 0;
    
}

AcWing 1175. 最大半連通子圖

#include <iostream>
#include <cstring>
#include <algorithm>
#include <unordered_set>
#include <stack>

using namespace std;

const int N = 1e5 + 10, M = 2 * 1e6 + 10;
typedef long long LL;
int h[N], hs[N], e[M], ne[M], idx;

int timecnt;
int dfn[N], low[N];
stack<int> stk;
int scc_cnt;
int in_stk[N];
int sizes[N];
int id[N];

int f[N], g[N];
unordered_set<LL> used;
int n, m, mod;

void add(int h[], int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

void tarjan(int u)
{
    dfn[u] = low[u] = ++timecnt;
    
    stk.push(u);
    in_stk[u] = 1;
    
    for (int i = h[u]; i != -1; i = ne[i]){
        int j = e[i];
        
        if (!dfn[j]){
            tarjan(j);
            low[u] = min(low[u], low[j]);
        }else if (in_stk[j])
                low[u] = min(low[u], dfn[j]);
    }
    
    if (low[u] == dfn[u]){
        scc_cnt ++;
        int y;
        do{
            y = stk.top();
            stk.pop();
            in_stk[y] = 0;
            id[y] = scc_cnt;
            sizes[scc_cnt] ++;
        }while (y != u);
    }
    
}

int main()
{
    cin >> n >> m >> mod;
    memset (h, -1, sizeof h);
    memset (hs, -1, sizeof hs);
    
    for (int i = 1; i <= m; i ++ ){
        int a, b;
        scanf("%d%d",&a, &b);
        add(h, a, b);
    }
    
    for (int i = 1; i <= n; i ++ )
        if (!dfn[i])
            tarjan(i);
            
    for (int i = 1; i <= n; i ++ )
        for (int j = h[i]; j != -1; j = ne[j]){
            int k = e[j];
            
            int a = id[i], b = id[k];
            if (a != b && !used.count((LL)a * 1e6 + b)){
                add(hs, a, b);
                used.insert((LL)a * 1e6 + b);
            }
        }
    
    for (int i = scc_cnt; i ; i -- ){
        if (!f[i]){
            f[i] = sizes[i];
            g[i] = 1;
        }
        for (int j = hs[i]; j != -1; j = ne[j]){
            int k = e[j];
            if (f[k] < f[i] + sizes[k]){
                f[k] = f[i] + sizes[k];
                g[k] = g[i];
            }else if (f[k] == f[i] + sizes[k])
                    g[k] = (g[k] + g[i]) % mod;
        }
    }
    
    int maxn = 0;
    int sum = 0;
    
    for (int i = 1; i <= scc_cnt; i ++ )
        if (f[i] > maxn){
            maxn = f[i];
            sum = g[i];
        }else if (f[i] == maxn)
            sum = (g[i] + sum) % mod;
    
    cout << maxn << endl << sum << endl;
    
    return 0;
}

AcWing 368. 銀河

算法思路:

強連通份量求解差分約束問題:
由強連通份量進行縮點, 求縮點後的拓撲圖, 對拓撲圖求最長路ci

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <stack>

using namespace std;

const int N = 1e5 + 10, M = 5e5 + 10;

int h[N], e[M], w[M], idx, ne[M];
int hs[N];          //縮點後的表頭

int low[N], dfn[N], timecnt;
int in_stk[N];
int scc_cnt;
int id[N];
int sizes[N];
stack<int> stk;
int dist[N];

int n, m;

void add(int a, int b, int v)
{
    e[idx] = b, ne[idx] = h[a], w[idx] = v, h[a] = idx ++;
}

void add1(int a, int b, int v)
{
    e[idx] = b, w[idx] = v, ne[idx] = hs[a], hs[a] = idx ++;
}

void tarjan(int u)
{
    low[u] = dfn[u] = ++timecnt;
    stk.push(u);
    in_stk[u] = 1;
    
    for (int i = h[u]; i != -1; i = ne[i]){
        int j = e[i];
        
        if (!dfn[j]){
            tarjan(j);
            
            low[u] = min(low[j], low[u]);
            
        }else if (in_stk[j])
            low[u] = min(low[u], dfn[j]);
    }
    
    if (low[u] == dfn[u]){
        scc_cnt ++;
        int y;
        do{
            y = stk.top();
            stk.pop();
            in_stk[y] = 0;
            id[y] = scc_cnt;
            sizes[scc_cnt] ++;
        }while (y != u);
    }
}

int main()
{
    cin >> n >> m;
    memset (h, -1, sizeof h);
    memset (hs, -1, sizeof hs);
    
    for (int i = 1; i <= m; i ++ ){
        int a, b, t;
        scanf("%d%d%d",&t, &a, &b);
        if (t == 1)
            add(a, b, 0), add(b, a, 0);
        if (t == 2)
            add(a, b, 1);
        if (t == 3)
            add(b, a, 0);
        if (t == 4)
            add(b, a, 1);
        if (t == 5)
            add(a, b, 0);
    }
    
    for (int i = 1; i <= n; i ++ )
        add(0, i, 1);
    
    tarjan(0);
    
    
    int flag = 1;        
    for (int i = 0; i <= n; i ++ )
        for (int j = h[i]; j != -1; j = ne[j]){
            int k = e[j];
            
            int a = id[i], b = id[k];
            if (a == b){
                if (w[j] > 0){
                    flag = 0;
                }
            }else add1(a, b, w[j]);
        }
        
    if (flag == 0)
        puts("-1");
    else{
        for (int i = scc_cnt; i > 0; i -- ){
            for (int j = hs[i]; j != -1; j = ne[j]){
                int k = e[j];
                
                dist[k] = max(dist[i] + w[j], dist[k]);
            }
        }
        
        long long res = 0;
        
        for (int i = 1; i <= scc_cnt; i ++ )
            res += dist[i] * sizes[i];
        cout << res << endl;
    }
    
    return 0;
    
}
相關文章
相關標籤/搜索