OJ: HDU

题目 ID: 3394

标签: 双连通分量todo

日期: 2026-01-09 14:43

todo: 再次读取代码,写一遍代码,读题,理解题目

样例解释

回路分析

  1. 回路 A:0–1–2–3–0(4 条边)
  2. 回路 B:4–5–6–7–4(4 条边)
  3. 回路 C:4–5–7–4(3 条边)
  4. 回路 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. 题目核心含义

题目描述了一个公园,包含 NN 个地点和 MM 条路(无向边)。

管理者想要沿着这些路修建铁路,并安排从起点回到起点的“观光路线”(即图中的环/回路)。

  • “Railway belongs to none tourist route” (不需要建的铁路):

    这意味着这条边不在任何环中。在图论中,这种边被称为 桥 (Bridge)。

  • “Railway belongs to more than one tourist route” (可能会冲突的铁路):

    这意味着这条边被多个环共用。

  • 隐含的第三种情况:

    如果一条边恰好属于一个环,它是安全的,既不需要剔除也不算冲突。

2. 算法模型:点双连通分量 (v-BCC)

我们需要使用 Tarjan 算法 求出图中所有的 点双连通分量 (v-BCC)

什么是点双连通分量?

在一个无向图中,如果一个子图内的任意两个点之间至少存在两条“点不重复”的路径,这个子图就是一个点双连通分量。

简单来说,点双连通分量是由边组成的极大集合,这些边“纠缠”在一起形成环。

利用 BCC 的性质判断边的情况:

对于每一个求出的 BCC,我们统计其中包含的边数 (EbccE_{bcc}) 和 点数 (VbccV_{bcc})。

  1. Ebcc<VbccE_{bcc} < V_{bcc}

    这种情况只发生在 Ebcc=1,Vbcc=2E_{bcc}=1, V_{bcc}=2 时。

    说明这个分量只有一条边,且构不成环。这条边就是 桥。

    对应答案:不需要建的铁路 (No need)。

  2. Ebcc=VbccE_{bcc} = V_{bcc}

    说明这个分量是一个 简单环(Simple Cycle)。

    其中的所有边都恰好属于这一个环。

    对应答案:既不冲突,也是必须的。

  3. Ebcc>VbccE_{bcc} > V_{bcc}

    说明这个分量内部边数多于点数,结构复杂,由多个环互相交织而成。

    在这个分量内的所有边,通常都被视为属于多个环(或者处于冲突区域)。

    对应答案:可能冲突的铁路 (Clash)。

3. 算法步骤

  1. 初始化:使用链式前向星存图,准备 Tarjan 所需的 dfn, low 数组,以及一个用于存储边的栈 stack
  2. Tarjan 遍历
    • 遍历每个连通块(防止图不连通)。
    • 在 DFS 过程中,将遍历到的压入栈中。
    • 当发现 low[v] >= dfn[u] 时,说明找到了一个 BCC。
  3. 处理 BCC
    • 从栈中弹出边,直到弹出当前边 (u,v)(u, v) 为止。这些弹出的边构成了当前的 BCC。
    • 统计这些边涉及的唯一点的数量 (VcntV_{cnt}) 和的数量 (EcntE_{cnt})。
    • 根据 EcntE_{cnt}VcntV_{cnt} 的关系更新答案:
      • Ecnt<VcntE_{cnt} < V_{cnt}ans1 += E_{cnt} (其实就是 +1)
      • Ecnt>VcntE_{cnt} > V_{cnt}ans2 += E_{cnt}
  4. 输出结果

4. 代码实现

这个代码超时,应该是maxn 开太大了

cpp
/**
 * 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;
}

这个代码正确

cpp
/**
 * 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. 复杂度分析

  • 时间复杂度O(N+M)O(N + M)。Tarjan 算法遍历每个点和边一次。处理 BCC 时,每条边进栈出栈一次,统计点数的操作与边数成正比。
  • 空间复杂度O(N+M)O(N + M)。用于存储图结构和递归栈。

6. 易错点

  • 重边判断:题目保证 “no loop and no multiple edges”,所以不需要特殊处理重边。如果题目有重边,逻辑会稍微复杂一些(需要用边 ID 而不是点来判断反向边)。
  • 统计点数:在处理栈弹出的边时,要利用 visit_token 或者 set 来去重统计点数,因为一条边连接两个点,多条边可能共享顶点。
  • 边栈的使用:Tarjan 求点双(v-BCC)时,栈里面存的是而不是点,这样处理起来最方便,因为点双的定义是边的集合。