【学习笔记】连通性相关

· · 算法·理论

强连通分量定义

强连通是指在有向图中任意两节点 u,v 可相互到达,我们称 u,v 两点强连通

强连通分量(Strongly Connected Compoments,SCC)是指极大的强连通子图。

如何求强连通分量

Ex.1 P3387 【模板】缩点

缩点之后建出新图,在新图上跑 spfa 求最长路,缩点同时统计强连通分量中的点权和。

#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int MAXN = 2e5 + 33;
int n, m, A[MAXN], head[MAXN], idx, From[MAXN], To[MAXN], new_head[MAXN], Ans, Sum[MAXN];
struct Edge {
    int v, nxt;
    Edge (int v = 0, int nxt = 0):v(v), nxt(nxt){};
} edge[MAXN], new_edge[MAXN];
inline void Addedge (Edge *edge, int *head, int u, int v) { edge[++ idx] = Edge (v, head[u]), head[u] = idx; }

namespace Tarjan {
    int dfn[MAXN], low[MAXN], sccnt, Col[MAXN], stk[MAXN], top, ind;
    bool inStk[MAXN];
    void tarjan (int u) {
        dfn[u] = low[u] = ++ ind;
        stk[++ top] = u, inStk[u] = 1;
        for (int i = head[u]; ~i; i = edge[i].nxt) {
            int v = edge[i].v;
            if (!dfn[v]) {
                tarjan (v);
                low[u] = min (low[u], low[v]);
            } else if (inStk[v]) {
                low[u] = min (low[u], dfn[v]);
            }
        }
        if (!(dfn[u] ^ low[u])) {
            sccnt ++;
            while (stk[top + 1] ^ u) {
                inStk[stk[top]] = 0;
                Sum[sccnt] += A[stk[top]];
                Col[stk[top --]] = sccnt;
            }
        }
    }
} using namespace Tarjan;

namespace Spfa {
    int Dis[MAXN]; bool vis[MAXN];
    queue <int> q;
    inline void initDis() { memset (Dis, -0x3f, sizeof (Dis)); }
    inline void spfa (int s) {
        initDis(), q.push (s);
        Dis[s] = Sum[s], vis[s] = 1;
        while (!q.empty()) {
            int u = q.front(); q.pop();
            vis[u] = 0;
            for (int i = new_head[u]; ~i; i = new_edge[i].nxt) {
                int v = new_edge[i].v;
                if (Dis[v] < Dis[u] + Sum[v]) {
                    Dis[v] = Dis[u] + Sum[v];
                    if (!vis[v]) {
                        vis[v] = 1;
                        q.push (v);
                    }
                }
            }
        }
        for (int i = 1; i <= sccnt; i ++)
            Ans = max (Ans, Dis[i]);
    }
} using namespace Spfa;

signed main() {
    scanf ("%lld %lld", &n, &m);
    memset (head, -1, sizeof (head));
    for (int i = 1; i <= n; i ++)
        scanf ("%lld", &A[i]);
    for (int i = 1; i <= m; i ++) {
        scanf ("%lld %lld", &From[i], &To[i]);
        Addedge (edge, head, From[i], To[i]);
    }
    for (int i = 1; i <= n; i ++) {
        if (!dfn[i])
            tarjan (i);
    }
    memset (new_head, -1, sizeof (new_head)), idx = 0;
    for (int i = 1; i <= m; i ++) {
        if (Col[From[i]] ^ Col[To[i]])
            Addedge (new_edge, new_head, Col[From[i]], Col[To[i]]);
    }
    for (int i = 1; i <= sccnt; i ++) spfa (i);
    printf ("%lld\n", Ans);
    return 0;
}

这里要注意的是:

Tarjan 优先会访问出度为 0 的点,也就是说最后缩点出来的 DAG 中强连通分量的编号是逆拓扑序。

Ex.2 P7251 [JSOI2014] 强连通图

对于第一个问题,答案就是最大的 \text{SCC} 的大小。

对于第二个问题,因为选择出度为 0 的点最后还是会要连入度为 0 的点,因此反而选择两者之中最大值最优。

#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int MAXN = 3e5 + 33;
int n, m, head[MAXN], idx, U[MAXN], V[MAXN];
struct EDGE {
    int v, nxt;
    EDGE (int v = 0, int nxt = 0):v(v), nxt(nxt){};
} Edge[MAXN << 1];
inline void AddEdge (int u, int v) { Edge[idx] = EDGE (v, head[u]), head[u] = idx ++; }

int dfn[MAXN], low[MAXN], sccnt, Col[MAXN], ind, stk[MAXN], top, Siz[MAXN];
bool inStk[MAXN], In[MAXN], Out[MAXN];
void Tarjan (int u) {
    dfn[u] = low[u] = ++ ind;
    stk[++ top] = u, inStk[u] = 1;
    for (int i = head[u]; ~i; i = Edge[i].nxt) {
        int v = Edge[i].v;
        if (!dfn[v]) {
            Tarjan(v);
            low[u] = min (low[u], low[v]);
        } else if (inStk[v]) {
            low[u] = min (low[u], dfn[v]);
        }
    }
    if (dfn[u] == low[u]) {
        sccnt ++;
        while (stk[top + 1] ^ u) {
            Siz[sccnt] ++;
            Col[stk[top]] = sccnt;
            inStk[stk[top --]] = 0;
        }
    }
}

signed main() {
    scanf ("%lld %lld", &n, &m);
    memset (head, -1, sizeof (head));
    for (int i = 1, u, v; i <= m; i ++) {
        scanf ("%lld %lld", &u, &v);
        AddEdge (u, v), U[i] = u, V[i] = v;
    }
    for (int i = 1; i <= n; i ++) {
        if (!dfn[i])
            Tarjan(i);
    }
    int Ans1 = 0, Ans2 = 0;
    for (int i = 1; i <= sccnt; i ++)
        Ans1 = max (Ans1, Siz[i]);
    for (int i = 1; i <= m; i ++) {
        if (Col[U[i]] ^ Col[V[i]]) 
            In[Col[V[i]]] = Out[Col[U[i]]] = 1;
    }
    int tot_In = 0, tot_Out = 0;
    for (int i = 1; i <= sccnt; i ++) {
        if (!In[i]) {
            tot_In ++;
        } if (!Out[i]) {
            tot_Out ++;
        }
    }
    Ans2 = ((sccnt ^ 1) ? max (tot_In, tot_Out) : 0);
    printf ("%lld\n%lld\n", Ans1, Ans2);
    return 0;
}

双联通分量

双联通分量分为点双联通分量边双联通分量,注意,以上两个都在无向图中讨论。

以下是一个无向图。

我们来讨论 A,B,C 三点的双连通性。

割点和割边

以下均在无向图中讨论。

如何求割点

求割点的过程就体现了 Tarjan 算法在连通性问题中的通用性。

如下图,给每个点打上时间戳。

红色的边是非树边(不在 DFS 生成树上),容易看出 4 号点是割点,考虑如何求。

首先我们明确一点,Tarjan 中我们会维护 dfn_u,low_u,分别表示 u 访问的时间戳以及 u 能回到的最早的祖先节点(不经过父亲),因此如果说 low_v \geq dfn_u,意为子节点无法回到祖先,且最多回到父亲,那么该节点可能成为割点。

为什么是可能呢?

实际上我们要考虑的就是搜索起始点,即子树的根的特殊情况。

代码长这样:

void Tarjan (int u, int fa) {
    dfn[u] = low[u] = ++ ind;
    inStk[u] = 1;
    int SonCnt = 0;
    for (int i = head[u]; ~i; i = edge[i].nxt) {
        int v = edge[i].v;
        if (!inStk[v]) {
            SonCnt ++; // 记录子节点个数
            Tarjan (v, u);
            low[u] = min (low[u], low[v]);
            if (low[v] >= dfn[u] && (u ^ fa) && !isNode[u]) // 如果说不是自己且满足割点的条件且没有被标记过
                isNode[u] = 1, NodeCnt ++;
        } else if (v ^ fa) {
            low[u] = min (low[u], dfn[v]);
        }
    }
    if (u == fa && SonCnt >= 2 && !isNode[u]) // 如果是自己且子树大小 >= 2 且没有被标记过
        isNode[u] = 1, NodeCnt ++;
} 

如何求割边(桥)

如下图。

红色的边为割边。

同理,如果 low_v > dfn_u 意为子节点回不到祖先节点(包括父亲),那么连接 (u,v) 的这条边就是割边。

代码长这样:

void Tarjan (int u, int fa) {
    dfn[u] = low[u] = ++ ind;
    for (int i = Head[u]; ~i; i = Edge[i].nxt) {
        int v = Edge[i].v;
        if (!dfn[v]) {
            Tarjan (v, u);
            low[u] = min (low[u], low[v]);
            if (low[v] > dfn[u]) 
                isBridge[i] = 1, cnt ++;
        } else if (v ^ fa) {
            low[u] = min (low[u], dfn[v]);
        }
    }
}

上述情况仅限于图中无重边的情况,有重边该怎么办呢?

你会发现只需要打一个标记即可。

代码长这样:

void Tarjan (int u, int fa) {
    bool flag = false;
    dfn[u] = low[u] = ++ ind;
    for (int i = Head[u]; ~i; i = Edge[i].nxt) {
        int v = Edge[i].v;
        if (!dfn[v]) {
            Tarjan (v, u);
            low[u] = min (low[u], low[v]);
            if (low[v] > dfn[u]) 
                isBridge[i] = 1, cnt ++;
        } else {
            if (v ^ fa || !flag) {
                low[u] = min (low[u], dfn[v]);
            } else {
                flag = true;
            }
        }
    }
}

如何求双联通分量

前置知识就是割点和割边。

例题

Ex.1 P4742 [Wind Festival] Running In The Sky

这题有环,有向图中的环就是一个强连通分量,所以我们可以用 Tarjan 缩点建新图,在新图上拓扑序 dp。

分别用两个 dp 数组维护路径最大亮度和最长路。

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN = 5e5 + 10;
int n, m, K[MAXN], Head[MAXN], idx, newHead[MAXN], newidx;
struct DAG {
    int v, nxt;
    DAG (int v = 0, int nxt = 0):v(v), nxt(nxt){};
} Edge[MAXN], newEdge[MAXN];
inline void AddEdge (int u, int v) { Edge[++ idx] = DAG (v, Head[u]), Head[u] = idx; }
inline void newAddEdge (int u, int v) { newEdge[++ newidx] = DAG (v, newHead[u]), newHead[u] = newidx; }

int dfn[MAXN], low[MAXN], sccnt, Col[MAXN], stk[MAXN], top, ind, sumVal[MAXN], maxVal[MAXN];
bool vis[MAXN];
void Tarjan (int u) {
    dfn[u] = low[u] = ++ ind;
    stk[++ top] = u, vis[u] = 1;
    for (int i = Head[u]; ~i; i = Edge[i].nxt) {
        int v = Edge[i].v;
        if (!dfn[v]) {
            Tarjan (v);
            low[u] = min (low[u], low[v]);
        } else if (vis[v]) {
            low[u] = min (low[u], dfn[v]);
        }
    }
    if (dfn[u] == low[u]) {
        sccnt ++;
        while (stk[top + 1] ^ u) {
            Col[stk[top]] = sccnt;
            sumVal[sccnt] += K[stk[top]];
            maxVal[sccnt] = max (maxVal[sccnt], K[stk[top]]);
            vis[stk[top --]] = 0;
        }
    }
}

int dpSum[MAXN], _u[MAXN], _v[MAXN], in[MAXN], dpMax[MAXN];
inline void tpsort() {
    queue <int> q;
    for (int i = 1; i <= sccnt; i ++) {
        if (!in[i]) q.push (i);
        dpSum[i] = sumVal[i], dpMax[i] = maxVal[i];
    }
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int i = newHead[u]; ~i; i = newEdge[i].nxt) {
            int v = newEdge[i].v;
            if (!(-- in[v])) q.push (v);
            if (dpSum[v] < dpSum[u] + sumVal[v]) {
                dpSum[v] = dpSum[u] + sumVal[v];
                dpMax[v] = max (dpMax[u], maxVal[v]);
            } else if (dpSum[v] == dpSum[u] + sumVal[v]) {
                dpMax[v] = max (dpMax[v], dpMax[u]);
            }
        }
    }
}

signed main() {
    scanf ("%lld %lld", &n, &m);
    memset (Head, -1, sizeof (Head));
    memset (newHead, -1, sizeof (newHead));
    memset (maxVal, -1, sizeof (maxVal));
    memset (dpSum, -0x3f, sizeof (dpSum));
    memset (dpMax, -1, sizeof (dpMax));
    for (int i = 1; i <= n; i ++)
        scanf ("%lld", &K[i]);
    for (int i = 1, u, v; i <= m; i ++) {
        scanf ("%lld %lld", &u, &v);
        AddEdge (u, v);
        _u[i] = u, _v[i] = v;
    }
    for (int i = 1; i <= n; i ++) {
        if (!dfn[i]) Tarjan (i);
    }
    for (int u = 1; u <= n; u ++) {
        for (int i = Head[u]; ~i; i = Edge[i].nxt) {
            int v = Edge[i].v;
            if (Col[u] ^ Col[v])
                newAddEdge (Col[u], Col[v]), in[Col[v]] ++;
        }
    }
    tpsort();
    int Ans = 1;
    for (int i = 2; i <= sccnt; i ++) {
        if (dpSum[i] > dpSum[Ans] || (dpSum[i] == dpSum[Ans] && dpMax[i] > dpMax[Ans])) Ans = i;
    }
    printf ("%lld %lld\n", dpSum[Ans], dpMax[Ans]);
    return 0;
}

Ex.2 P5058 [ZJOI2004] 嗅探器

我们要找能够隔开 A,B 的割点,将问题放到搜索树上。

首先明确一点,如果说 vu 的子树内,应满足 dfn_v \leq dfn_u,利用这一点,我们就去找既是割点又满足 dfn_x \leq dfn_A \text{ and } dfn_x >dfn_B 或者 dfn_x \leq dfn_B \text{ and } dfn_x >dfn_A 的最小编号的点。

#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int MAXN = 5e5 + 10;
int n, A, B, Head[MAXN], idx;
struct Graph {
    int v, nxt;
    Graph (int v = 0, int nxt = 0):v(v), nxt(nxt){};
} Edge[MAXN];
inline void AddEdge (int u, int v) { Edge[idx] = Graph (v, Head[u]), Head[u] = idx ++; }

inline void readin() {
    scanf ("%lld", &n);
    memset (Head, -1, sizeof (Head));
    while (true) {
        int u, v;
        scanf ("%lld %lld", &u, &v);
        if (!u && !v) break;
        AddEdge (u, v), AddEdge (v, u);
    }
    scanf ("%lld %lld", &A, &B);
}

int dfn[MAXN], low[MAXN], ind, Ans = 0x7f7f7f7f;
inline bool Chk_inTree (int u) {
    if (dfn[u] <= dfn[A] && dfn[u] > dfn[B]) return true;
    if (dfn[u] <= dfn[B] && dfn[u] > dfn[A]) return true;
    return false;
}
void Tarjan (int u, int fa) {
    dfn[u] = low[u] = ++ ind;
    for (int i = Head[u]; ~i; i = Edge[i].nxt) {
        int v = Edge[i].v;
        if (!dfn[v]) {
            Tarjan (v, u);
            low[u] = min (low[u], low[v]);
            if (low[v] >= dfn[u] && u ^ A && u ^ B && Chk_inTree (u)) Ans = min (Ans, u);
        } else if (v ^ fa) {
            low[u] = min (low[u], dfn[v]);
        }
    }
}

inline void Work() {
    Tarjan (1, 0);
    if (Ans == 0x7f7f7f7f) {
        printf ("No solution\n");
    } else {
        printf ("%lld\n", Ans);
    }
}

signed main() {
    readin();
    Work();
    return 0;
}

Ex.3 P2272 [ZJOI2007] 最大半连通子图

原图有环,所以缩点建新图。

根据最大半联通子图的定义,我们推出其实就是要求一个最长链,所以拓扑序 dp 即可。

#include <bits/stdc++.h>
#define int long long
using namespace std;
inline int read() {
    int res = 0, f = 1;
    char ch = getchar();
    while (!isdigit(ch)) f = ch == '-' ? -1 : 1, ch = getchar();
    while (isdigit(ch)) res = (res << 1) + (res << 3) + (ch ^ 48), ch = getchar();
    return res * f;
}
const int MAXN = 1e6 + 10;
int n, m, mod, Head[MAXN], idx, newHead[MAXN], newidx;
struct DAG {
    int v, nxt;
    DAG (int v = 0, int nxt = 0):v(v), nxt(nxt){};
} Edge[MAXN], newEdge[MAXN];

inline void AddEdge (int u, int v) { Edge[++ idx] = DAG (v, Head[u]), Head[u] = idx; }
inline void newAddEdge (int u, int v) { newEdge[++ newidx] = DAG (v, newHead[u]), newHead[u] = newidx; }

int dfn[MAXN], low[MAXN], stk[MAXN], Siz[MAXN], Col[MAXN], sccnt, top, ind;
bool vis[MAXN];
void Tarjan (int u) {
    dfn[u] = low[u] = ++ ind;
    stk[++ top] = u, vis[u] = 1;
    for (int i = Head[u]; ~i; i = Edge[i].nxt) {
        int v = Edge[i].v;
        if (!dfn[v]) {
            Tarjan (v);
            low[u] = min (low[u], low[v]);
        } else if (vis[v]) {
            low[u] = min (low[u], dfn[v]);
        }
    }
    if (dfn[u] == low[u]) {
        sccnt ++;
        while (stk[top + 1] ^ u) {
            Col[stk[top]] = sccnt;
            Siz[sccnt] ++;
            vis[stk[top --]] = 0;
        }
    }
}

int dpSiz[MAXN], dpCnt[MAXN], in[MAXN], tag[MAXN];
inline void tpSort() {
    for (int i = sccnt; i; i --)
        dpSiz[i] = Siz[i], dpCnt[i] = 1;
    for (int u = sccnt; u; u --) {
        for (int i = newHead[u]; ~i; i = newEdge[i].nxt) {
            int v = newEdge[i].v;
            if (tag[v] == u) continue;
            tag[v] = u;
            if (dpSiz[v] < dpSiz[u] + Siz[v]) {
                dpSiz[v] = dpSiz[u] + Siz[v];
                dpCnt[v] = dpCnt[u];
            } else if (dpSiz[v] == dpSiz[u] + Siz[v]) {
                dpCnt[v] = (dpCnt[v] + dpCnt[u]) % mod;
            }
        }
    }
}

signed main() {
    n = read(), m = read(), mod = read();
    memset (Head, -1, sizeof (Head));
    memset (newHead, -1, sizeof (newHead));
    for (int i = 1; i <= m; i ++) {
        int u = read(), v = read();
        AddEdge (u, v);
    }
    for (int i = 1; i <= n; i ++) {
        if (!dfn[i]) Tarjan (i);
    }
    for (int u = 1; u <= n; u ++) {
        for (int i = Head[u]; ~i; i = Edge[i].nxt) {
            int v = Edge[i].v;
            if (Col[u] ^ Col[v]) {
                newAddEdge (Col[u], Col[v]);
                in[Col[v]] ++;
            }
        }
    }
    tpSort();
    int AnsSiz = 0, AnsCnt = 0;
    for (int i = 1; i <= sccnt; i ++) {
        if (dpSiz[i] > AnsSiz) {
            AnsSiz = dpSiz[i], AnsCnt = dpCnt[i];
        } else if (dpSiz[i] == AnsSiz) {
            AnsCnt = (AnsCnt + dpCnt[i]) % mod;
        }
    }
    printf ("%lld\n%lld\n", AnsSiz, AnsCnt);
    return 0;
}

结语

推荐题单 1

推荐题单 2

qwq。