用 Tarjan 分解强连通分量,分量内任意两城都构成便利城市对。
OJ: shumeng
题目 ID: CSP201509D
难度:普及+/提高-
标签:强连通分量Tarjan有向图
日期: 2026-07-31 16:21
形式化题目
给定一个
思路
先看一个小数据基准:对每个城市做一次 BFS,直接检查所有城市对是否互相可达。
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-31 16:21
* update_at: 2026-08-17 22:57
*/
// brute.cpp:对每个城市做 BFS,直接检查互相可达的城市对。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
vector<vector<int> > graph(n + 1);
for (int i = 0; i < m; i++) {
int u, v;
cin >> u >> v;
graph[u].push_back(v);
}
// reach[i][j] 表示从城市 i 出发能否到达城市 j。
vector<vector<int> > reach(n + 1, vector<int>(n + 1));
for (int start = 1; start <= n; start++) {
queue<int> q;
q.push(start);
reach[start][start] = 1;
while (!q.empty()) {
int u = q.front(); q.pop();
for (int i = 0; i < (int)graph[u].size(); i++) {
int v = graph[u][i];
if (!reach[start][v]) {
reach[start][v] = 1;
q.push(v);
}
}
}
}
// 统计互相可达的无序城市对。
long long answer = 0;
for (int i = 1; i <= n; i++) for (int j = i + 1; j <= n; j++) answer += reach[i][j] && reach[j][i];
cout << answer << '\n';
return 0;
}brute.cpp 用一个 reach[i][j] 表格记录可达关系,再两两判断,时间复杂度为
关键观察
互相可达的两个城市恰好位于同一个强连通分量:分量内任意两点两两互通,分量之间则单向可达、无法互相到达。
Tarjan 求强连通分量
用 Tarjan 算法找出所有强连通分量。每找到一个大小为
累加所有分量的贡献即为答案。注意 long long 存储。
代码
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-31 16:21
* update_at: 2026-08-17 22:57
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10005;
vector<int> graph[MAXN]; // 邻接表存有向图
int dfn[MAXN], low[MAXN]; // 首次访问时间戳与可达的最早时间戳
int in_stack[MAXN]; // 节点是否在 Tarjan 的栈中
int stack_nodes[MAXN]; // Tarjan 栈
int clock_time, stack_size;
long long answer; // 便利城市对总数,用 long long 防溢出
// Tarjan 求强连通分量:栈中同一分量会连续弹出。
void tarjan(int u) {
dfn[u] = low[u] = ++clock_time;
stack_nodes[++stack_size] = u;
in_stack[u] = 1;
for (int i = 0; i < (int)graph[u].size(); i++) {
int v = graph[u][i];
if (dfn[v] == 0) {
// 树边:递归后尝试用子树的可达时间更新 low[u]。
tarjan(v);
low[u] = min(low[u], low[v]);
} else if (in_stack[v]) {
// 回边或横叉边指向栈中节点:说明它们处于同一分量。
low[u] = min(low[u], dfn[v]);
}
}
// low[u] == dfn[u] 时,u 是所在分量的根,栈顶到 u 之间是完整分量。
if (low[u] != dfn[u]) return;
int size = 0;
while (true) {
int v = stack_nodes[stack_size--];
in_stack[v] = 0;
size++;
if (v == u) break;
}
// 分量内任意两点两两互通,贡献 C(size, 2) 对。
answer += 1LL * size * (size - 1) / 2;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
while (m--) {
int u, v;
cin >> u >> v;
graph[u].push_back(v);
}
for (int i = 1; i <= n; i++) if (dfn[i] == 0) tarjan(i);
cout << answer << '\n';
return 0;
}复杂度
- 时间:Tarjan 每个点和边各访问常数次,
。 - 空间:邻接表与 Tarjan 栈,
。
总结
“双向可达”直接对应强连通分量,这是有向图问题最常见的转化。求出分量大小后,城市对数量由组合数一次性算出,不需要逐一枚举点对。
