数据结构与算法-图论-强连通分量(tarjan算法)

发布于:2025-03-26 ⋅ 阅读:(23) ⋅ 点赞:(0)

概念

在有向图 G 中,如果对于任意两个顶点 u 和 v,都存在从 u 到 v 以及从 v 到 u 的路径,则称图 G 是强连通图。强连通分量是有向图中的极大强连通子图,即该子图是强连通的,且不存在包含该子图且更大的强连通子图。

实现算法

Floyd 算法

基本思想:Floyd 算法是一种动态规划算法,用于计算图中任意两点之间的最短路径。对于每一对顶点 i 和 j,考虑是否通过中间顶点 k 能使路径更短,即 dist[i][j]=min(dist[i][j],dist[i][k]+dist[k][j])。

代码:

#include <iostream>#include <vector>#include <climits>

using namespace std;

// Floyd 算法计算任意两点间最短路径

vector<vector<int>> floyd(const vector<vector<int>>& graph) {

    int n = graph.size();

    vector<vector<int>> dist(n, vector<int>(n, INT_MAX));

    // 初始化距离矩阵

    for (int i = 0; i < n; ++i) {

        for (int j = 0; j < n; ++j) {

            if (i == j) {

                dist[i][j] = 0;

            } else if (graph[i][j] != 0) {

                dist[i][j] = graph[i][j];

            }

        }

    }

    // 动态规划更新距离矩阵

    for (int k = 0; k < n; ++k) {

        for (int i = 0; i < n; ++i) {

            for (int j = 0; j < n; ++j) {

                if (dist[i][k] != INT_MAX && dist[k][j] != INT_MAX) {

                    dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);

                }

            }

        }

    }

    return dist;}

Tarjan 算法

基本思想:Tarjan 算法基于深度优先搜索(DFS),利用两个数组 dfn 和 low 来记录顶点的访问顺序和能追溯到的最早栈中顶点的时间戳。当 dfn[u] == low[u] 时,说明找到了一个强连通分量的根,通过栈来收集该强连通分量中的所有顶点。

#include <iostream>#include <vector>#include <stack>using namespace std;

const int N = 10005;

vector<int> graph[N];

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

bool inStack[N];

stack<int> st;

color[N];

void tarjan(int u) {

    dfn[u] = low[u] = ++index;//标记时间戳

    st.push(u);//入栈

    inStack[u] = true;//标记在栈中

    for (int v : graph[u]) {//搜索子节点

        if (!dfn[v]) {//如果没有搜索过,就搜索,然后更新在stack中最早的节点

            tarjan(v);

            low[u] = min(low[u], low[v]);

        } else if (inStack[v]) {//如果已搜过,根据时间戳来更新

            low[u] = min(low[u], dfn[v]);

        }

    }

if (dfn[u] == low[u]) {//如果是根的话,那么就去统计当前强连通分量中的点

        cnt++;//所点后的编号

            while (true) {

             int v = st.top();

             color[v]=cnt;

             st.pop();

             inStack[v] = false;

             if (v == u) break;

            }

}

}

功能:

缩点

缩点是将有向图中的强连通分量压缩成一个点的操作。经过缩点后,得到的新图是一个有向无环图(DAG),便于进行拓扑排序和动态规划等操作。

图的连通性分析

判断图的强连通性:可以通过计算有向图的强连通分量来判断整个图是否是强连通的。如果图中只有一个强连通分量,并且该强连通分量包含图中的所有顶点,那么这个有向图就是强连通图。例如,在一个表示城市间单向道路的有向图中,如果它是强连通的,意味着任意两个城市之间都可以相互到达。

分析图的连通结构:了解图中各个强连通分量之间的关系,可以清晰地把握图的整体连通结构。比如,不同的强连通分量可以看作是图中的 “连通块”,它们之间通过一些有向边相互连接。这有助于对复杂网络进行分层和模块化分析,像在社交网络中,不同的强连通分量可能代表不同的社交圈子。

求解传递闭包问题

传递闭包概念:在有向图中,传递闭包描述了顶点之间是否存在可达关系。如果从顶点 u 到顶点 v 存在一条路径,那么在传递闭包中就认为 u 到 v 是可达的。

强连通分量的应用:通过找出图中的强连通分量,同一强连通分量内的顶点相互可达。在此基础上,可以进一步分析不同强连通分量之间的可达性,从而构建图的传递闭包。例如,在知识图谱中,节点代表知识概念,边代表概念之间的关系,通过强连通分量和传递闭包的分析,可以发现知识之间的潜在联系。

检测环和回路

环的存在判断:强连通分量与图中的环密切相关。如果一个强连通分量包含至少两个顶点,那么这个强连通分量中必然存在环。因为强连通分量中的任意两个顶点之间都相互可达,所以一定存在一个包含这些顶点的回路。

环的提取和分析:在复杂的有向图中,检测环和回路对于很多问题都非常重要。例如,在程序的控制流图中,环可能表示程序中的循环结构;在电路设计中,环可能会导致信号的无限循环,需要进行特殊处理。通过找出强连通分量,可以准确地定位和分析图中的环。

路径规划和优化

有效路径筛选:在路径规划问题中,强连通分量可以帮助筛选出有效的路径。例如,在一个交通网络中,如果两个地点位于不同的强连通分量中,且它们之间没有直接或间接的可达路径,那么就可以避免在这两个地点之间进行无效的路径搜索。

路径优化策略:对于位于同一强连通分量内的顶点,可以根据强连通分量的特性设计更高效的路径优化策略。比如,在一个物流网络中,同一强连通分量内的配送点可以采用循环配送的方式,提高配送效率。

图的简化和抽象

去除冗余信息:强连通分量可以帮助识别图中的冗余部分。在一个大型有向图中,某些强连通分量可能是相对独立的子结构,对整体的分析和处理影响较小。通过将这些强连通分量进行适当的简化或抽象,可以减少图的复杂度,提高算法的效率。

构建抽象模型:基于强连通分量的分析结果,可以构建图的抽象模型。例如,在一个复杂的通信网络中,可以将每个强连通分量抽象为一个节点,将不同强连通分量之间的连接抽象为边,从而得到一个简化的抽象图,便于进行宏观的分析和决策。

解决问题

路径规划问题:在交通网络等实际场景中,可确定哪些区域相互可达,避免无效搜索。

任务调度问题:分析任务间的依赖关系,合理安排任务执行顺序。

网络分析:分析社交网络或计算机网络中用户群体或节点的紧密程度和相互影响关系。

练习题:

受欢迎的牛:

题目描述

每头奶牛都梦想成为牛棚里的明星。被所有奶牛喜欢的奶牛就是一头明星奶牛。所有奶牛都是自恋狂,每头奶牛总是喜欢自己的。奶牛之间的“喜欢”是可以传递的——如果 A 喜欢 B,B 喜欢 C,那么 A 也喜欢 C。牛栏里共有 N 头奶牛,给定一些奶牛之间的爱慕关系,请你算出有多少头奶牛可以当明星。

输入格式

第一行:两个用空格分开的整数:N 和 M。

接下来 M 行:每行两个用空格分开的整数:A 和 B,表示 A 喜欢 B。

输出格式

一行单独一个整数,表示明星奶牛的数量。

分析:

通过scc来进行缩点,由题目可以知道,如果存在2个及以上的出度为0的点,就不存在明星牛牛,最多只能有1个出度为0的牛牛,所以我们要统计的是,出度为0的牛牛的数量,他们就是明星牛!

代码:

#include <bits/stdc++.h>

using namespace std;

const int N = 10010;

int id[N];

int dfn[N], low[N];

unordered_map<int, vector<int>> e;

int cnt = 1;

int stk[N];

int sz[N];

int top = -1;

int id_cnt = 0;

int n, m;

int out[N];  // 记录每个 SCC 的出度

void tarjan(int u) {

    dfn[u] = low[u] = cnt++;

    stk[++top] = u;

    for (int v : e[u]) {

        if (!dfn[v]) {

            tarjan(v);

            low[u] = min(low[u], low[v]);

        } else if (!id[v]) {

            low[u] = min(low[u], dfn[v]);

        }

    }

    if (dfn[u] == low[u]) {

        id_cnt++;

        int tmp;

        do {

            tmp = stk[top--];

            id[tmp] = id_cnt;

            sz[id_cnt]++;

        } while (tmp != u);

    }

}

int main() {

    scanf("%d%d", &n, &m);

    for (int i = 1; i <= m; i++) {

        int u, v;

        scanf("%d%d", &u, &v);

        e[u].push_back(v);

    }

    for (int i = 1; i <= n; i++) {

        if (!dfn[i]) tarjan(i);

    }

    for (int i = 1; i <= n; i++) {

        for (int j : e[i]) {

            if (id[i] != id[j]) out[id[i]]++;  // 统计出度

        }

    }

    int res = 0;

    int zero_out_cnt = 0;  // 出度为 0 的 SCC 的数量

    for (int i = 1; i <= id_cnt; i++) {

        if (out[i] == 0) {

            res += sz[i];

            zero_out_cnt++;

        }

    }

    // 如果有多个出度为 0 的 SCC,则没有明星奶牛

    cout << (zero_out_cnt <=1 ? res : 0) << endl;

    return 0;

}

学校网络

题目背景

浙江省的几所 OI 强校的神犇发明了一种人工智能,可以 AC 任何题目,所以他们决定建立一个网络来共享这个软件。但是由于他们脑力劳动过多导致全身无力身体被♂掏♂空,他们来找你帮助他们。

题目描述

共有 n 所学校 (1≤n≤10000) 已知他们实现设计好的网络共 m 条线路,为了保证高速,网络是单向的。现在请你告诉他们至少选几所学校作为共享软件的母机,能使每所学校都可以用上。再告诉他们至少要添加几条线路能使任意一所学校作为母机都可以使别的学校使用上软件。

输入格式

第一行一个正整数 n。

接下来 n 行每行有若干个整数,用空格隔开。

第 i+1 行,每行输入若干个非零整数 x,表示从 i 到 x 有一条线路。以 0 作为结束标志。

输出格式

第一行一个整数,表示至少选几所学校作为共享软件的母机,能使每所学校都可以用上。

第二行一个整数,表示至少要添加几条线路能使任意一所学校作为母机都可以使别的学校使用上软件。

分析:


使用tarjan算法进行缩点,然后根据缩点后新的图进行下一步判断

第一个问题就是问的入度为0的点有多少个,第二个问题要求我们要满足将所有的起点or终点加入其中,所以答案是max(入度为0,出度为0)

代码:


#include<bits/stdc++.h>

using namespace std;

const int N=100010;

int dfn[N],low[N];

int id[N],idcnt=0;

int stk[N],top=-1;

int cnt=0;

bool instk[N];

unordered_map<int,vector<int>> e;

int n;

int in[N],out[N];

void tarjan(int u){

    dfn[u]=low[u]=++cnt;

    stk[++top]=u;

    instk[u]=1;

    for(int v:e[u]){

        if(instk[v]){

            low[u]=min(dfn[v],low[u]);

        }else if(!dfn[v]){

            tarjan(v);

            low[u]=min(low[u],low[v]);

        }

    }

    if(dfn[u]==low[u]){

        idcnt++;

        int tmp;

        do{

            tmp=stk[top--];

            id[tmp]=idcnt;

            instk[tmp]=0;

        }while(tmp!=u);

    }

}

int main(){

    scanf("%d",&n);

    for(int i=1;i<=n;i++){

        int tmp;

        while(scanf("%d",&tmp),tmp!=0){

            e[i].push_back(tmp);

        }

    }

    for(int i=1;i<=n;i++){

        if(!dfn[i]){

            tarjan(i);

        }

    }

    

    for(int i=1;i<=n;i++){

        for(int j:e[i]){

            if(id[i]!=id[j]){

                out[id[i]]++;

                in[id[j]]++;

            }

        }

    }

    int res1=0,res2=0;

    for(int i=1;i<=idcnt;i++){

        if(in[i]==0)res1++;

        if(out[i]==0)res2++;

    }

    if (idcnt == 1) {

        cout << res1 << endl << 0;

    } else {

        cout << res1 << endl << max(res1, res2);

    }

}

最大半连通子图:

题目描述

一个有向图 G=(V,E) 称为半连通的 (Semi-Connected),如果满足:∀u,v∈V,满足 u→v 或 v→u,即对于图中任意两点 u,v,存在一条 u 到 v 的有向路径或者从 v 到 u 的有向路径。

若 G′=(V′,E′) 满足 V′⊆V,E′ 是 E 中所有跟 V′ 有关的边,则称 G′ 是 G 的一个导出子图。若 G′ 是 G 的导出子图,且 G′ 半连通,则称 G′ 为 G 的半连通子图。若 G′ 是 G 所有半连通子图中包含节点数最多的,则称 G′ 是 G 的最大半连通子图。

给定一个有向图 G,请求出 G 的最大半连通子图拥有的节点数 K,以及不同的最大半连通子图的数目 C。由于 C 可能比较大,仅要求输出 C 对 X 的余数。

输入格式

第一行包含两个整数 N,M,X。N,M分别表示图 G 的点数与边数,X 的意义如上文所述。

接下来 M 行,每行两个正整数 a,b,表示一条有向边 (a,b)。图中的每个点将编号为 1,2,3…N,保证输入中同一个(a,b)不会出现两次。

输出格式

应包含两行,第一行包含一个整数 K,第二行包含整数 CmodX。

分析:

tarjan求缩点,根据拓扑序来动态规划

trick:

1-tarjan的逆序就是拓扑序

2-通过unordered_set判断新图重边

代码:

#include <bits/stdc++.h>

using namespace std;

const int N = 100010;

int x;

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

int stk[N], top = -1;

bool instk[N];

int id[N], idcnt = 0;

int sz[N];

int n, m;

int f[N], g[N];

unordered_map<int, vector<int>> e, ne;

// Tarjan 算法求强连通分量

void tarjan(int u) {

    dfn[u] = low[u] = ++tt;

    stk[++top] = u;

    instk[u] = true;

    for (int v : e[u]) {

        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]) {

        idcnt++;

        int tmp;

        do {

            tmp = stk[top--];

            sz[idcnt]++;

            instk[tmp] = false;

            id[tmp] = idcnt;

        } while (tmp != u);

    }

}

int main() {

    scanf("%d%d%d", &n, &m, &x);

    for (int i = 0; i < m; i++) {

        int u, v;

        scanf("%d%d", &u, &v);

        e[u].push_back(v);

    }

    // 求强连通分量

    for (int i = 1; i <= n; i++) {

        if (!dfn[i]) tarjan(i);

    }

    // 构建新图

    unordered_set<long long> edges;

    for (int i = 1; i <= n; i++) {

        for (int j : e[i]) {

            int a = id[i], b = id[j];

            if (a != b) {

                long long hash = (long long)a * N + b;

                if (!edges.count(hash)) {

                    ne[a].push_back(b);

                    edges.insert(hash);

                }

            }

        }

    }

    // 动态规划

    for (int i = idcnt; i; i--) {

        if (!f[i]) {

            f[i] = sz[i];

            g[i] = 1;

        }

        for (int j : ne[i]) {

            if (f[j] < f[i] + sz[j]) {

                f[j] = f[i] + sz[j];

                g[j] = g[i];

            } else if (f[j] == f[i] + sz[j]) {

                g[j] = (g[j] + g[i]) % x;

            }

        }

    }

    // 找出最大节点数和方案数

    int ans = 0, cnt = 0;

    for (int i = 1; i <= idcnt; i++) {

        if (ans < f[i]) {

            ans = f[i];

            cnt = g[i];

        } else if (ans == f[i]) {

            cnt = (cnt + g[i]) % x;

        }

    }

    // 输出结果

    cout << ans << endl << cnt << endl;

    return 0;

}