强连通份量

有向图的强连通份量

基本概念

连通份量:

对于份量内任意两点\(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;
    
}
相关文章
相关标签/搜索