强连通分量

Tarjan算法

求强连通分量。
算法思路:在DFS森林上进行操作,不断地整合点成块然后切割块,使得只考虑经过但未被切割的点,这样就忽略了横向边的影响。只要一个点能返祖(自己或经过子树)到更小的时间点,那么这个点就属于某一个强连通块;否则该点就作为这个强连通块的第一个被搜索到的点,强连通块的其他点的返祖最小时间点也一定是该点,那么可以切割并记录该强连通分量。
时间复杂度 $O(N+M)$

vector<int> edge[N];

int dfn[N], low[N], ins[N], idx = 0; //分别代表到i点的时间,返祖最小时间,是否在堆中(或被切割)
stack<int> stk; //记录经过但待分类的点
vector<vector<int>> scc; //记录强连通分量

void dfs(int u){
    dfn[u] = low[u] = ++ idx;
    ins[u] = true;
    stk.push(u);
    for (auto v : edge[u])
        if (!dfn[v]){ //没搜过
            dfs(v);
            low[u] = min(low[u], low[v]);
        }else{
            if (ins[v]) low[u] = min(low[u], dfn[v]); //不考虑指向另外强连通分量的横向边,只考虑状态未定的点
        }
    if (dfn[u] == low[u]){ //强连通分量的First节点
        vector<int> c;
        while (true){
            int v = stk.top(); //取出
            c.push_back(v); //记录
            ins[v] = false;//被切割
            stk.pop(); //删除
            if (v == u) break;
        }
        //sort(c.begin(), c.end());
        scc.push_back(c);
    }
}

Kosaraju 算法

相比于Tarjan算法,只需要记录一个点有没有救,即纳入强连通分量的潜力。

Important

DAG(有向无环图)DFS一遍的出栈顺序是反图的拓扑序
有向图 SCC缩点 -> DAG
推论:在缩点完的DAG里一定是个源点(没有入度)

算法思路:2遍DFS,dfs_1对正图求出栈顺序out,然后翻转out,遍历对其dfs_2对反图求出所有以该源点的强连通分量。
时间复杂度 $O(N+M)$

vector<int> e[N], erev[N]; //原图 反图

int vis[N];
vector<int> c, out;

void dfs_1(int u){
    vis[u] = true;
    for (auto v : e[u])
        if (!vis[v]) dfs_1(v);
    out.push_back(u);
}

void dfs_2(int u){
    vis[u] = true;
    c.push_back(u);
    for (auto v : erev[u])
        if (!vis[v]) dfs_2(v);
}

void solve() {
    int n, m;
    cin >> n >> m;
    while (m -- ){
        int x, y;
        cin >> x >> y;
        e[x].push_back(y);
        erev[y].push_back(x);
    }   
    for (int i = 1; i <= n; i ++ )
        if (!vis[i]) dfs_1(i);
    reverse(out.begin(), out.end());
    memset(vis, 0, sizeof vis);
    for (auto u : out)
        if (!vis[u]){
            c.clear();
            dfs_2(u);
        }
}

(桥)割边 和 割点

Tarjan算法
对于一个点X,考虑其子树的所有返祖边返不到自己先祖

vector<pair<int, int>> e[N]; // u -> v 边的编号为second

int dfn[N], low[N], idx = 0;

vector<int> bridge;
//fa -> u 的 id
void dfs(int u, int id){
    dfn[u] = low[u] = ++ idx;
    for (auto [v, id2] : e[u]){
        if (!dfn[v]) dfs(v, id2);
        if (id != id2) low[u] = min(low[u], low[v]); //判重 使用id可以判重 但fa不能判重
    }
    if (dfn[u] == low[u] && id != -1){ //注意id != -1
        bridge.push_back(id);
    }
}

void solve(){
    int n, m;
    for (int i = 1; i <= n; i ++ ) e[i].clear(), dfn[i] = low[i] = 0; //初始化
    for (int i = 1; i <= m; i ++ ){
        int u, v;
        cin >> u >> v;
        e[u].push_back({v, i});
        e[v].push_back({u, i});
    }
    bridge.clear();
    for (int i = 1; i <= n; i ++ ) //考虑多个连通块
        if (!dfn[i]) dfs(i, -1);
    cout << bridge.size() << '\n';
}

割点

思考点与点的关系,不能用dfn[u] == low[v]判断割点

vector<int> e[N];

int dfn[N], low[N], idx, sz;
int cut[N];

void dfs(int u, int fa){
    dfn[u] = low[u] = ++ idx;
    int ch = 0;
    for (auto v : e[u]){
        if (!dfn[v]){
            dfs(v, u);
            ++ ch;
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) cut[u] = 1; //是割点   **最关键的判断**
        }else if (v != fa){
            low[u] = min(low[u], dfn[v]);
        }
    }
    if (fa == -1 && ch <= 1) cut[u] = 0; //根特殊
    sz += cut[u];
}

//以下是读入,可随意修改
void solve(){
    int n;
    while (cin >> n && n){
        for (int i = 1; i <= n; i ++ ) e[i].clear(), dfn[i] = low[i] = cut[i] = 0; //初始化
        int u;
        while (cin >> u && u){
            while (getchar() != '\n'){ //判断读行
                int v;
                cin >> v;
                e[u].push_back(v);
                e[v].push_back(u);
            }
        }
        idx = sz = 0;
        for (int i = 1; i <= n; i ++ )
            if (!dfn[i]) dfs(i, -1);
        cout << sz << '\n';
    }
}

DP有关

可以直接在缩点过程中DP
样例:

#include<bits/stdc++.h>
#define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)

using namespace std;

const int N = 5e5 + 10;
// SCC缩点成DAG进行DP

vector<int> e[N];

int dfn[N], low[N], ins[N], idx = 0;
stack<int> stk;

int cnt, T;
int dp[N], bel[N], vis[N], isd[N];

int val[N], ed[N];

void dfs(int u){
    dfn[u] = low[u] = ++ idx;
    ins[u] = true;
    stk.push(u);
    for (auto v : e[u])
        if (!dfn[v]) {
            dfs(v);
            low[u] = min(low[u], low[v]);
        }else{
            if (ins[v]) low[u] = min(low[u], dfn[v]);
        }
    if (dfn[u] == low[u]){
        ++ cnt;
        dp[cnt] = 0, isd[cnt] = 0;
        ++ T;
        vis[cnt] = T;
        int sum = 0, is_ed = 0;
        while (true){
            int v = stk.top();
            ins[v] = false;
            bel[v] = cnt;
            sum += val[v];
            if (ed[v]) {
                is_ed = true;
                //cout << "终点!" << v << '\n';
            }
            for (auto w : e[v])
                if (vis[bel[w]] != T && isd[bel[w]]){
                    is_ed = true;
                    vis[bel[w]] = T;
                    dp[cnt] = max(dp[cnt], dp[bel[w]]);
                }
            stk.pop();
            if (v == u) break;
        }
        dp[cnt] += sum;
        isd[cnt] = is_ed;
        //cout << cnt << ' ' << dp[cnt] << ' ' << isd[cnt] << '\n';
    }
}

void solve() {
    int n, m;
    cin >> n >> m;
    while (m -- ){
        int x, y;
        cin >> x >> y;
        e[x].push_back(y);
    }
    for (int i = 1; i <= n; i ++ ) cin >> val[i];
    int S, P;
    cin >> S >> P;
    while (P--){
        int x;
        cin >> x;
        ed[x] = true;
    }
    for (int i = 1; i <= n; i ++ )
        if (!dfn[i]) dfs(i);
    cout << dp[bel[S]];
}

signed main() {
    IOS;
    int t = 1;
//    cin >> t;
    while (t--) {
        solve();
    }
    return 0;
}