在图论中,强连通分量(Strongly Connected Components, SCC)是一个重要的概念。强连通分量是指在一个有向图中,一组顶点的任意两点之间都存在双向可达路径的顶点的集合。在实际应用中,如社交网络分析、网络流量分析等方面,强连通分量的计算具有广泛的应用。C++中,Kosaraju算法和Tarjan算法是两种常用的求解强连通分量的算法。
Kosaraju算法是一个基于深度优先搜索(DFS)的算法,其基本原理分为两步:
-
第一次DFS遍历:从任意节点开始进行深度优先搜索,并对遍历过的节点进行标记。搜索结束后,可以得到一个节点的逆后序序列。
-
第二次DFS遍历:根据逆后序序列,从后往前对每个节点进行深度优先搜索,并标记属于同一个强连通分量的节点。
示例:求出每个节点所属的强连通分量编号,代码如下。
cpp
#include <iostream>
#include <vector>
#include <stack>
#include <cstring>
using namespace std;
vector<int> adj[100]; // 邻接表存储图
bool visited[100]; // 标记节点是否被访问过
int scc[100]; // 存储每个节点所属的强连通分量编号
int scc_count; // 强连通分量计数器
stack<int> stk; // 用于存储DFS遍历的节点
// 第一次DFS遍历,得到逆后序序列
void dfs1(int v) {
visited[v] = true;
for (int i = 0; i < adj[v].size(); i++) {
int u = adj[v][i];
if (!visited[u]) {
dfs1(u);
}
}
stk.push(v); // 将节点压入栈中,得到逆后序序列
}
// 第二次DFS遍历,标记强连通分量
void dfs2(int v, int id) {
visited[v] = true;
scc[v] = id; // 标记节点所属的强连通分量编号
for (int i = 0; i < adj[v].size(); i++) {
int u = adj[v][i];
if (!visited[u]) {
dfs2(u, id);
}
}
}
// Kosaraju算法主函数
void kosaraju() {
memset(visited, false, sizeof(visited));
for (int i = 0; i < 100; i++) { // 假设节点编号从0到99
if (!visited[i]) {
dfs1(i); // 第一次DFS遍历
}
}
memset(visited, false, sizeof(visited));
scc_count = 0;
while (!stk.empty()) {
int v = stk.top();
stk.pop();
if (!visited[v]) {
dfs2(v, scc_count++); // 第二次DFS遍历,并更新强连通分量计数器
}
}
}
int main() {
// 构建图(以邻接表形式)
adj[0].push_back(1);
adj[1].push_back(2);
adj[2].push_back(0);
adj[2].push_back(3);
adj[3].push_back(3);
kosaraju(); // 调用Kosaraju算法
// 输出每个节点所属的强连通分量编号
for (int i = 0; i < 4; i++) {
cout << "Node " << i << " belongs to SCC " << scc[i] << endl;
}
return 0;
}
Tarjan算法是一种基于深度优先搜索和栈的算法,通过维护一个栈和一个访问时间戳数组来实现强连通分量的求解。其基本步骤如下:
-
初始化所有节点的访问状态为未访问,时间戳为0,最低访问时间戳为无穷大。
-
对每个未访问的节点进行深度优先搜索,同时更新访问状态和时间戳。
-
如果一个节点的后继节点已经在栈中且其最低访问时间戳小于当前节点的访问时间戳,则找到了一个强连通分量。
-
通过不断回溯和弹出栈中元素,直到不再满足上述条件为止,从而得到一个强连通分量。
-
重复上述步骤,直到所有节点都被访问过。
示例:求出每个强连通分量,代码如下。
cpp
#include <iostream>
#include <vector>
#include <stack>
#include <cstring>
using namespace std;
vector<int> adj[100]; // 邻接表存储图
int index; // 时间戳计数器
int low[100]; // 节点能够回溯到的最早时间戳
bool instack[100]; // 节点是否在栈中
int scc_count; // 强连通分量计数器
stack<int> stk; // 用于DFS遍历的栈
vector<int> scc_list[100]; // 存储每个强连通分量的节点
void tarjan(int v) {
int i;
low[v] = index++;
stk.push(v);
instack[v] = true;
for (i = 0; i < adj[v].size(); i++) {
int u = adj[v][i];
if (low[u] == -1) { // 如果u未访问过
tarjan(u);
if (low[u] < low[v]) {
low[v] = low[u];
}
} else if (instack[u]) { // 如果u在栈中
if (low[u] < low[v]) {
low[v] = low[u];
}
}
}
if (low[v] == index - 1) { // 发现一个强连通分量
int j;
do {
j = stk.top();
stk.pop();
instack[j] = false;
scc_list[scc_count].push_back(j);
} while (j != v);
scc_count++;
}
}
void tarjan_algorithm() {
memset(low, -1, sizeof(low));
memset(instack, false, sizeof(instack));
index = 0;
scc_count = 0;
for (int i = 0; i < 100; i++) { // 假设节点编号从0到99
if (low[i] == -1) {
tarjan(i); // 对每个未访问的节点进行Tarjan算法
}
}
}
int main() {
// 构建图(以邻接表形式)
adj[0].push_back(1);
adj[1].push_back(2);
adj[2].push_back(0);
adj[2].push_back(3);
adj[3].push_back(3);
tarjan_algorithm(); // 调用Tarjan算法
// 输出每个强连通分量
for (int i = 0; i < scc_count; i++) {
cout << "SCC " << i << ": ";
for (int j = 0; j < scc_list[i].size(); j++) {
cout << scc_list[i][j] << " ";
}
cout << endl;
}
return 0;
}
Kosaraju算法和Tarjan算法都是用于求解有向图强连通分量的经典算法。Kosaraju算法通过两次深度优先搜索来求解,而Tarjan算法则利用时间戳和栈来实现更高效的求解。在实际应用中,可以根据图的特点和具体需求选择合适的算法。