todo: 再次读取代码,写一遍代码,读题,理解题目
样例解释
回路分析
- 回路 A:0–1–2–3–0(4 条边)
- 回路 B:4–5–6–7–4(4 条边)
- 回路 C:4–5–7–4(3 条边)
- 回路 D:5–6–7–5(3 条边)
注意:回路 C 和 D 共享边 5–7 和 7–4(C 有 4–5, 5–7, 7–4;D 有 5–6, 6–7, 7–5)。 实际上,边 7–4 在回路 B 和 C 中都出现。 边 5–7 在回路 C 和 D 中都出现。
冲突边判断
一条边如果属于 至少两个不同的简单回路(在同一个点双连通分量中,但被多个环共用),则它是冲突的。
- 边 7–4:在回路 B 和 C 中 → 冲突
- 边 5–7:在回路 C 和 D 中 → 冲突
- 边 4–5:在回路 B 和 C 中 → 冲突
- 边 6–7:在回路 B 和 D 中 → 冲突
- 边 5–6:在回路 B 和 D 中 → 冲突
所以冲突边共 5 条: (4,5), (5,6), (6,7), (7,4), (5,7)
不在任何回路中的边
- 边 3–4:它连接了两个不同的点双连通分量(0-1-2-3 和 4-5-6-7),因此不在任何回路中。 所以只有 1 条这样的边。
绘制图(Graphviz)
graph G {
rankdir=LR;
node [shape=circle];
0 -- 1;
1 -- 2;
2 -- 3;
3 -- 0;
3 -- 4 [color="red", penwidth=2]; // 不在任何回路中
4 -- 5 [color="blue", penwidth=2]; // 冲突边
5 -- 6 [color="blue", penwidth=2]; // 冲突边
6 -- 7 [color="blue", penwidth=2]; // 冲突边
7 -- 4 [color="blue", penwidth=2]; // 冲突边
5 -- 7 [color="blue", penwidth=2]; // 冲突边
// 高亮回路
subgraph cluster_0 {
color=lightgrey;
style=filled;
0; 1; 2; 3;
label="回路 0-1-2-3-0";
}
subgraph cluster_1 {
color=lightblue;
style=filled;
4; 5; 6; 7;
label="包含多个回路 (4-5-6-7-4, 4-5-7-4, 5-6-7-5)";
}
}
图说明:
- 红色边(3–4)是 不在任何回路中 的边(只有 1 条)。
- 蓝色边是 冲突边(5 条)。
- 灰色区域是第一个回路(0-1-2-3-0)。
- 浅蓝色区域是另一个点双连通分量,内部有多个回路,导致多条边被多个回路共用。
题目解析:Railway (HDU 3394)
这道题考察的是 图论中的双连通分量 (Biconnected Components, BCC) 性质。
1. 题目核心含义
题目描述了一个公园,包含
管理者想要沿着这些路修建铁路,并安排从起点回到起点的“观光路线”(即图中的环/回路)。
-
“Railway belongs to none tourist route” (不需要建的铁路):
这意味着这条边不在任何环中。在图论中,这种边被称为 桥 (Bridge)。
-
“Railway belongs to more than one tourist route” (可能会冲突的铁路):
这意味着这条边被多个环共用。
-
隐含的第三种情况:
如果一条边恰好属于一个环,它是安全的,既不需要剔除也不算冲突。
2. 算法模型:点双连通分量 (v-BCC)
我们需要使用 Tarjan 算法 求出图中所有的 点双连通分量 (v-BCC)。
什么是点双连通分量?
在一个无向图中,如果一个子图内的任意两个点之间至少存在两条“点不重复”的路径,这个子图就是一个点双连通分量。
简单来说,点双连通分量是由边组成的极大集合,这些边“纠缠”在一起形成环。
利用 BCC 的性质判断边的情况:
对于每一个求出的 BCC,我们统计其中包含的边数 (
-
: 这种情况只发生在
时。 说明这个分量只有一条边,且构不成环。这条边就是 桥。
对应答案:不需要建的铁路 (No need)。
-
: 说明这个分量是一个 简单环(Simple Cycle)。
其中的所有边都恰好属于这一个环。
对应答案:既不冲突,也是必须的。
-
: 说明这个分量内部边数多于点数,结构复杂,由多个环互相交织而成。
在这个分量内的所有边,通常都被视为属于多个环(或者处于冲突区域)。
对应答案:可能冲突的铁路 (Clash)。
3. 算法步骤
- 初始化:使用链式前向星存图,准备 Tarjan 所需的
dfn,low数组,以及一个用于存储边的栈stack。 - Tarjan 遍历:
- 遍历每个连通块(防止图不连通)。
- 在 DFS 过程中,将遍历到的边压入栈中。
- 当发现
low[v] >= dfn[u]时,说明找到了一个 BCC。
- 处理 BCC:
- 从栈中弹出边,直到弹出当前边
为止。这些弹出的边构成了当前的 BCC。 - 统计这些边涉及的唯一点的数量 (
) 和边的数量 ( )。 - 根据
和 的关系更新答案: - 若
: ans1 += E_{cnt}(其实就是 +1) - 若
: ans2 += E_{cnt}
- 若
- 从栈中弹出边,直到弹出当前边
- 输出结果。
4. 代码实现
这个代码超时,应该是maxn 开太大了
/**
* Author by Rainboy blog: https://rainboylv.com github : https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* date: 2026-01-09 15:42:58
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int maxn = 2e6+5;
const int maxe = 4e6+5;
const int mod = 1e9+7;
int n,m;
int cnt_v[maxn]; // 每个bcc 中的点数
int cnt_e[maxn]; // 每个bcc 中的边数
//oisnip_begine-bcc-two-pass.cpp
struct TarjanEBCC {
// ---------------- 图存储 ----------------
struct edge {
int u,v,next;
} e[maxe * 2];
int head[maxn], e_cnt;
// ---------------- 找桥变量 ----------------
int dfn[maxn], low[maxn], timer;
bool is_bridge[maxe * 2]; // 标记边是否为桥 (下标对应边的编号)
// ---------------- 染色变量 ----------------
int bcc_id[maxn]; // 每个点所属的分量编号 (1 ~ bcc_cnt)
int bcc_cnt; // 边双连通分量总数
void init(int n) {
e_cnt = 0;
timer = 0;
bcc_cnt = 0;
for (int i = 0; i <= n; i++) {
head[i] = -1;
dfn[i] = low[i] = 0;
bcc_id[i] = 0;
}
// 注意:is_bridge 需要清空到最大边数
// 如果多组数据且边数变化大,建议用 loop 清空或 vector
memset(is_bridge, 0, sizeof(is_bridge));
}
void add_edge(int u, int v) {
e[e_cnt] = {u,v, head[u]};
head[u] = e_cnt++;
e[e_cnt] = {v,u, head[v]};
head[v] = e_cnt++;
}
// pass 1: tarjan 找桥
// in_edge: 进入 u 的那条边的编号 (用于处理重边)
void tarjan(int u, int fa) {
// std::cout << u << "\n";
dfn[u] = low[u] = ++timer;
for (int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v;
if( v == fa) continue;
// 只要不是反向边 (即不走刚才来的路)
if (!dfn[v]) {
tarjan(v, u);
low[u] = min(low[u], low[v]);
if (low[v] > dfn[u]) {
is_bridge[i] = is_bridge[i ^ 1] = true;
// std::cout << "bridge: " << " ";
// std::cout << u << " " << v << "\n";
}
} else {
low[u] = min(low[u], dfn[v]);
}
}
}
// pass 2: dfs 染色
void dfs_color(int u, int id) {
bcc_id[u] = id;
for (int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v;
// 如果该边不是桥,且对面没被染过色,则继续
if (!is_bridge[i] && !bcc_id[v]) {
dfs_color(v, id);
}
}
}
// 主过程
void solve(int n) {
// 1. 找桥
for (int i = 1; i <= n; i++) {
if (!dfn[i]) tarjan(i, -1);
}
// 2. 染色
for (int i = 1; i <= n; i++) {
if (!bcc_id[i]) {
bcc_cnt++;
dfs_color(i, bcc_cnt);
}
}
}
} ;
//oisnip_end
TarjanEBCC ebcc;
void init(){
ebcc.init(n);
for(int i = 1;i <= m ;++i ) // i: 1->m
{
int u,v;
std::cin >> u >> v;
ebcc.add_edge(u+1, v+1);
}
}
signed main () {
ios::sync_with_stdio(false); cin.tie(0);
while (1) {
std::cin >> n >> m;
if( n == 0 && m == 0) break;
init();
ebcc.solve(n);
memset(cnt_v,0,sizeof(cnt_v));
memset(cnt_e,0,sizeof(cnt_e));
// 1. 统计每个 BCC 的点数
for(int i = 1;i <= n ;++i ) // i: 1->n
{
if( ebcc.bcc_id[i]) cnt_v[ ebcc.bcc_id[i] ] ++;
}
int bridge_ans = 0;
int clash_ans = 0;
for(int i = 0;i < ebcc.e_cnt ;i+=2 ) // i: 0->e
{
if( ebcc.is_bridge[i] )
{
bridge_ans++;
}
else {
// 如果不是桥,属于某个 BCC
int u = ebcc.e[i].u;
// 累加该 BCC 的边数
cnt_e[ebcc.bcc_id[u]]++;
}
}
// 3. 计算冲突边
for(int i = 1; i <= ebcc.bcc_cnt; i++) {
// 如果 BCC 内 边数 > 点数,说明是复杂环,所有边冲突
if(cnt_e[i] > cnt_v[i]) {
clash_ans += cnt_e[i];
}
}
std::cout << bridge_ans << " " <<clash_ans << "\n";
}
return 0;
}
这个代码正确
/**
* Author by Rainboy
* Problem: HDU 3394 Railway
* Method: Tarjan v-BCC (Point-Biconnected Component)
*/
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstdio>
#include <stack>
#include <cstring>
using namespace std;
const int maxn = 10010;
const int maxe = 200020;
int n, m;
struct Edge {
int u, v, next, id;
} e[maxe];
int head[maxn], e_cnt;
int dfn[maxn], low[maxn], timer;
int bridge_ans, clash_ans;
// 栈存储边的 ID
int st[maxe], top;
// 标记数组,用于统计每个块内的点数 (避免重复计数)
int vis_tag[maxn], tag_id;
void init() {
e_cnt = 0;
timer = 0;
top = 0;
bridge_ans = 0;
clash_ans = 0;
tag_id = 0;
// 初始化 head, dfn, vis_tag
// 只需要初始化 0 到 n 的范围
for(int i = 0; i <= n; i++) {
head[i] = -1;
dfn[i] = 0;
low[i] = 0;
vis_tag[i] = 0;
}
}
void add_edge(int u, int v, int id) {
e[e_cnt].u = u;
e[e_cnt].v = v;
e[e_cnt].id = id; // 记录原始边的编号如果不必要可以省略,但这里用索引作为ID
e[e_cnt].next = head[u];
head[u] = e_cnt++;
}
// 处理找到的连通分量(块)
void process_bcc(int u, int v) {
int edge_count = 0;
int vertex_count = 0;
tag_id++; // 使用新的标记轮次,代替 memset
while (true) {
int idx = st[--top]; // 弹出栈顶边
edge_count++;
int curr_u = e[idx].u;
int curr_v = e[idx].v;
// 统计点数
if (vis_tag[curr_u] != tag_id) {
vis_tag[curr_u] = tag_id;
vertex_count++;
}
if (vis_tag[curr_v] != tag_id) {
vis_tag[curr_v] = tag_id;
vertex_count++;
}
// 直到弹出的边是当前边 (u, v) 为止
// 注意:因为是无向图存双边,边的ID判断要小心,这里简化逻辑:
// 只要弹出的不是我们刚才入栈的那个逻辑边就继续
// 实际上 tarjan 逻辑中,当 process_bcc 被调用时,栈顶一系列边就是该分量的
// 我们利用 idx 和当前边的关系来停止可能比较麻烦,不如直接利用 Tarjan 性质:
// 在 low[v] >= dfn[u] 时,栈中所有在 (u,v) 之上的边都属于这个分量
// 为了准确停止,我们通常比较 e[idx] 是否就是 e[i] (当前遍历的边)
// 但这里无法直接传 i,所以通常做法是:在入栈前记录栈顶位置,或者手动判断
// 修正:更简单的做法是,只要弹出的边不是当前递归触发的边,就一直弹
// 但这里为了方便,我们只判断值
if (curr_u == u && curr_v == v) break;
// 同时也可能是反向存的 v, u,但我们在 tarjan 里 push 的是当前的 directed edge
}
// 判定冲突
if (edge_count > vertex_count) {
clash_ans += edge_count;
}
}
void tarjan(int u, int fa_edge_idx) {
dfn[u] = low[u] = ++timer;
for (int i = head[u]; ~i; i = e[i].next) {
int v = e[i].v;
// 如果是反向边(父亲来的边),跳过
// 使用异或技巧:0^1=1, 1^1=0; 2^1=3, 3^1=2
if ((i ^ 1) == fa_edge_idx) continue;
if (!dfn[v]) {
st[top++] = i; // 将当前边入栈
tarjan(v, i);
low[u] = min(low[u], low[v]);
// 核心逻辑:Point-BCC 判断条件
if (low[v] >= dfn[u]) {
// 1. 判断是否为桥
if (low[v] > dfn[u]) {
bridge_ans++;
}
// 2. 弹出并处理该分量内的所有边
// 这里的处理逻辑:弹出直到栈顶是当前边 i
int edge_count = 0;
int vertex_count = 0;
tag_id++;
while (true) {
int idx = st[--top];
edge_count++;
int u_node = e[idx].u;
int v_node = e[idx].v;
if (vis_tag[u_node] != tag_id) {
vis_tag[u_node] = tag_id;
vertex_count++;
}
if (vis_tag[v_node] != tag_id) {
vis_tag[v_node] = tag_id;
vertex_count++;
}
if (idx == i) break; // 弹到了当前边,结束
}
if (edge_count > vertex_count) {
clash_ans += edge_count;
}
}
} else if (dfn[v] < dfn[u]) {
// 回边:更新 low,并入栈
st[top++] = i;
low[u] = min(low[u], dfn[v]);
}
}
}
int main() {
// 加上扩栈代码,防止爆栈 (HDU 特色)
// #pragma comment(linker, "/STACK:102400000,102400000")
while (scanf("%d%d", &n, &m) != EOF) {
if (n == 0 && m == 0) break;
init();
for (int i = 0; i < m; i++) {
int u, v;
scanf("%d%d", &u, &v);
// 0-based 转 1-based,防止冲突
u++; v++;
add_edge(u, v, i);
add_edge(v, u, i);
}
for (int i = 1; i <= n; i++) {
if (!dfn[i]) {
tarjan(i, -1);
}
}
printf("%d %d\n", bridge_ans, clash_ans);
}
return 0;
}
5. 复杂度分析
- 时间复杂度:
。Tarjan 算法遍历每个点和边一次。处理 BCC 时,每条边进栈出栈一次,统计点数的操作与边数成正比。 - 空间复杂度:
。用于存储图结构和递归栈。
6. 易错点
- 重边判断:题目保证 “no loop and no multiple edges”,所以不需要特殊处理重边。如果题目有重边,逻辑会稍微复杂一些(需要用边 ID 而不是点来判断反向边)。
- 统计点数:在处理栈弹出的边时,要利用
visit_token或者set来去重统计点数,因为一条边连接两个点,多条边可能共享顶点。 - 边栈的使用:Tarjan 求点双(v-BCC)时,栈里面存的是边而不是点,这样处理起来最方便,因为点双的定义是边的集合。