feat: 实现Tarjan算法查找点双连通分量

添加Tarjan算法实现来查找图中的点双连通分量(v-BCC)。包括处理孤立节点和自环边的情况,并输出每个v-BCC的节点列表。使用手动栈优化性能,并添加详细注释说明算法逻辑。
This commit is contained in:
Zengtudor 2025-10-02 20:20:19 +08:00
parent 1f4768391d
commit 9a06eb0821

View File

@ -1,9 +1,100 @@
#include <cstdint>
#include <iostream>
#include <vector>
#include <algorithm>
using ll = int64_t;
const int MAXN = 5e5+5; // 节点数上限
int n, m;
std::vector<int> adj[MAXN]; // 邻接表存图
int dfn[MAXN], low[MAXN], ts; // Tarjan算法所需的时间戳数组
int stk[MAXN], top; // 手动实现的栈比std::stack稍快
bool isbcc[MAXN]; // 标记节点是否已属于某个v-BCC
int main(){
std::vector<std::vector<int>> bcc; // 存储所有找到的点双连通分量
void tarjan(int u, int parent) {
dfn[u] = low[u] = ++ts;
stk[++top] = u;
// 对于孤立点若没有边则tarjan函数会直接结束。
// 我们将在main函数中处理这种情况。
for (int v : adj[u]) {
// 在无向图中,(u,v)和(v,u)是同一条边,避免访问刚过来的边
if (v == parent) {
continue;
}
if (!dfn[v]) { // v未被访问是u在DFS树中的子节点
tarjan(v, u);
low[u] = std::min(low[u], low[v]);
// 关键判断v及其子树无法到达u的祖先
if (low[v] >= dfn[u]) {
std::vector<int> curbcc;
int node;
// 从栈中弹出节点直到v被弹出
do {
node = stk[top--];
curbcc.push_back(node);
isbcc[node] = true;
} while (node != v);
// 割点u也属于这个v-BCC
curbcc.push_back(u);
isbcc[u] = true;
bcc.push_back(curbcc);
}
} else { // v已被访问(u,v)是返祖边
low[u] = std::min(low[u], dfn[v]);
}
}
}
int main() {
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cin >> n >> m;
for (int i = 0; i < m; ++i) {
int u, v;
std::cin >> u >> v;
// 题目允许重边和自环。对于v-BCC重边和一条边效果一样自环可特殊处理
// 此处我们正常建图让tarjan处理
if (u == v) continue; // v-BCC定义通常不考虑自环但题目样例有先忽略自环边最后处理孤立点
adj[u].push_back(v);
adj[v].push_back(u);
}
// 遍历所有节点,确保处理完所有连通分量
for (int i = 1; i <= n; ++i) {
if (!dfn[i]) {
// 如果一个节点是孤立的(或只与已访问过的部分相连),
// tarjan调用后栈可能为空或只含自己。
// 为了简化我们只在tarjan内部处理连通的组件。
tarjan(i, 0);
}
}
// 最后的清扫处理所有未被分配到任何v-BCC的节点
// 这些通常是孤立点,或者图中只有一个节点的情况,包括有自环的孤立点
for (int i = 1; i <= n; ++i) {
if (!isbcc[i]) {
bcc.push_back({i});
}
}
// 输出结果
std::cout << bcc.size() << "\n";
for (const auto& ret : bcc) {
std::cout << ret.size();
for (int node : ret) {
std::cout << " " << node;
}
std::cout << "\n";
}
}