题解:洛谷 P2860 Redundant Paths

【题目来源】

洛谷:P2860 [USACO06JAN] Redundant Paths G - 洛谷

【题目描述】

为了从 \(F(1\le F\le 5,000)\) 个牧场(编号为 \(1\)\(F\))中的一个到达另一个牧场,贝西和其他牛群被迫经过腐烂苹果树附近。奶牛们厌倦了经常被迫走特定的路径,想要修建一些新路径,以便在任意一对牧场之间总是有至少两条独立的路线可供选择。目前在每对牧场之间至少有一条路径,他们希望至少有两条。当然,他们只能在官方路径上从一个牧场移动到另一个牧场。

给定当前 \(R(F-1\le R\le 10,000)\) 条路径的描述,每条路径恰好连接两个不同的牧场,确定必须修建的最少新路径数量(每条新路径也恰好连接两个牧场),以便在任意一对牧场之间至少有两条独立的路线。若两条路线不使用相同的路径,即使它们沿途访问相同的中间牧场,也被视为独立的。

在同一对牧场之间可能已经有多条路径,你也可以修建一条新路径连接与某条现有路径相同的牧场。

【输入】

\(1\) 行:两个用空格分隔的整数:\(F\)\(R\)

\(2\) 行到第 \(R+1\) 行:每行包含两个用空格分隔的整数,表示某条路径的两个端点牧场。

【输出】

\(1\) 行:一个整数,表示必须修建的新路径数量。

【输入样例】

7 7
1 2
2 3
3 4
2 5
4 5
5 6
5 7

【输出样例】

2

【核心思想】

  1. 问题分析:给定一个包含 \(F\) 个牧场 \(R\) 条路径的无向连通图,需要确定最少修建多少条新路径,使得任意两个牧场之间至少有两条独立的路径(即图变为边双连通图)。这是一个边双连通分量缩点 + 树形结构分析问题。

  2. 算法选择

    • Tarjan 算法求桥和边双连通分量:找出图中所有的桥,将边双连通分量缩点
    • 缩点构建树:将每个边双连通分量看作一个节点,桥作为边,构建一棵树
    • 叶子节点统计:统计缩点后树中度数为 \(1\) 的节点(叶子)数量
    • 最少加边公式\((leaf + 1) / 2\),即把叶子两两配对连接
  3. 关键步骤

    • 建图:读入 \(F\)(节点数)、\(R\)(边数),使用邻接表存储无向图
    • Tarjan 求边双连通分量(从节点 \(x\) 出发,\(in_edg\) 为进入边):
      • 初始化 \(dfn[x] = low[x] = ++tot\),节点入栈
      • 遍历邻接点 \(y\)
        • 树边\(y\) 未访问):递归访问,回溯时 \(low[x] = \min(low[x], low[y])\)
        • 判断桥:若 \(low[y] > dfn[x]\),标记边 \(i\) 和反向边 \(i \oplus 1\) 为桥
        • 回边\(y\) 已访问且不是来时的边):\(low[x] = \min(low[x], dfn[y])\)
      • 缩点:若 \(dfn[x] == low[x]\),弹出栈中节点直到 \(x\),这些节点属于同一分量
    • 统计度数:遍历所有边,若边 \(i\) 是桥,则 \(d[con_id[e[i].v]]++\)(增加目标分量的度数)
    • 计算答案:统计度数为 \(1\) 的分量数量 \(cnt\),答案为 \((cnt + 1) / 2\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(F + R)\),Tarjan 算法线性时间
    • 空间复杂度:\(O(F + R)\),邻接表、\(dfn\)/\(low\) 数组、栈、分量信息
    • 适用于边双连通性分析和网络加固问题
  5. 边双连通分量缩点的核心思想

    • 边双连通分量:图中不存在桥的最大子图,分量内任意两点间有两条边不相交路径
    • 桥的作用:桥是连接不同边双连通分量的唯一通道,删除桥会使图断开
    • 缩点思想:将每个边双连通分量缩为一个节点,桥作为边,原图变成一棵树
    • 树的性质:缩点后的树中,叶子节点(度数为 \(1\))代表只通过一座桥与其他分量连接的分量
    • 最少加边策略:将叶子两两配对连接,\((leaf + 1) / 2\) 是最优解
    • 适用场景:网络可靠性分析、关键路径识别、网络加固优化

【解题思路】

img/Pasted image 20260521104755.png
img/Pasted image 20260521105142.png
img/Pasted image 20260521105417.png
img/Pasted image 20260521110008.png

【算法标签】

提高+ #双连通分量

【代码详解】

#include <bits/stdc++.h>
using namespace std;

const int N = 5005;        // 最大节点数
const int M = 20005;        // 最大边数(无向图需要两倍空间)

// 边结构体
struct edge
{
    int v;                  // 边的终点
    int ne;                 // 下一条边的索引
} e[M];

int h[N];                   // 邻接表头指针数组
int idx = 1;                // 边索引计数器(从2开始,便于异或操作)

int dfn[N];                 // DFS序(时间戳)
int low[N];                 // 通过回边能到达的最小DFN值
int tot;                    // 时间戳计数器

stack<int> stk;             // Tarjan算法栈

int dcc[N];                 // 存储每个节点所属的边双连通分量编号
int cnt;                    // 边双连通分量计数器

int bri[M];                 // 标记边是否为桥
int d[N];                   // 存储每个边双连通分量的度数

int n, m, a, b;             // n:节点数, m:边数, a,b:临时变量

/**
 * 添加无向边到邻接表
 * @param a 边的起点
 * @param b 边的终点
 */
void add(int a, int b)
{
    e[++idx].v = b;         // 记录边的终点
    e[idx].ne = h[a];       // 新边指向原头节点
    h[a] = idx;             // 更新头节点指向新边
}

/**
 * Tarjan算法求边双连通分量和桥
 * @param x 当前节点
 * @param in_edg 进入当前节点的边索引
 */
void tarjan(int x, int in_edg)
{
    // 初始化当前节点的DFN和LOW值
    dfn[x] = low[x] = ++tot;

    // 将当前节点压入栈
    stk.push(x);

    // 遍历当前节点的所有邻接节点
    for (int i = h[x]; i; i = e[i].ne)
    {
        int y = e[i].v;     // 邻接节点

        // 如果邻接节点y未被访问(树边)
        if (!dfn[y])
        {
            tarjan(y, i);                   // 递归访问y
            low[x] = min(low[x], low[y]);   // 更新low值

            // 桥的判断条件:low[y] > dfn[x]
            if (low[y] > dfn[x])
            {
                bri[i] = bri[i ^ 1] = true; // 标记边i和反向边i^1为桥
            }
        }
        // 如果y已被访问且不是来时的边(回边)
        else if (i != (in_edg ^ 1))
        {
            low[x] = min(low[x], dfn[y]);   // 通过回边更新low值
        }
    }

    // 如果当前节点是边双连通分量的根
    if (dfn[x] == low[x])
    {
        ++cnt;  // 增加边双连通分量计数

        // 弹出栈中节点直到遇到当前节点
        while (true)
        {
            int y = stk.top();
            stk.pop();
            dcc[y] = cnt;   // 记录节点所属的边双连通分量
            if (y == x)
            {
                break;
            }
        }
    }
}

int main()
{
    // 输入节点数和边数
    cin >> n >> m;

    // 输入所有边并构建无向图
    while (m--)
    {
        cin >> a >> b;
        add(a, b);  // 添加边a->b
        add(b, a);  // 添加边b->a(无向图需要双向添加)
    }

    // 执行Tarjan算法(假设图连通,从节点1开始)
    tarjan(1, 0);

    // 统计每个边双连通分量的度数(桥的数量)
    for (int i = 2; i <= idx; i++)
    {
        // 如果当前边是桥
        if (bri[i])
        {
            // 增加目标节点所在分量的度数
            d[dcc[e[i].v]]++;
        }
    }

    // 统计度数为1的边双连通分量数量(叶子节点)
    int sum = 0;
    for (int i = 1; i <= cnt; i++)
    {
        if (d[i] == 1)
        {
            sum++;
        }
    }

    // 输出需要添加的最少边数:(叶子节点数+1)/2
    cout << (sum + 1) / 2 << endl;

    return 0;
}
#include <bits/stdc++.h>
using namespace std;
const int N = 5005, M = 10005 * 2;  // 注意:边数要乘2,因为是无向图
int n, m;  // n: 节点数,m: 边数
int h[N], e[M], ne[M], idx;  // 邻接表存储图
int dfn[N], low[N], tot;  // dfn: DFS序,low: 追溯值,tot: 时间戳
stack<int> stk;  // 栈,用于Tarjan算法
int bri[M];  // 标记边是否为桥
int con_cnt, con_size[N], con_id[N];  // con_cnt: 边双连通分量数量,con_size: 分量大小,con_id: 节点所属分量编号
int d[N];  // 记录每个边双连通分量的度数
vector<int> ans[N];  // 存储每个边双连通分量的节点

// 添加无向边
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// Tarjan算法求边双连通分量
void tarjan(int x, int in_edg)
{
    dfn[x] = low[x] = ++tot;  // 初始化dfn和low为时间戳
    stk.push(x);  // 节点入栈

    for (int i = h[x]; i != -1; i = ne[i])  // 遍历x的所有邻接点
    {
        int y = e[i];  // 邻接点y
        if (!dfn[y])  // 如果y未被访问
        {
            tarjan(y, i);  // 递归访问y
            low[x] = min(low[x], low[y]);  // 更新low[x]
            if (low[y] > dfn[x])  // 判断边(x,y)是否为桥
                bri[i] = bri[i ^ 1] = true;  // 标记该边和其反向边为桥
        }
        else if (i != (in_edg ^ 1))  // 如果是回退边且不是反向边
        {
            low[x] = min(low[x], dfn[y]);  // 更新low[x]
        }
    }

    if (dfn[x] == low[x])  // 如果x是边双连通分量的根
    {
        con_cnt++;  // 增加边双连通分量计数
        while (1)  // 弹出栈中节点
        {
            int y = stk.top();  // 取出栈顶
            stk.pop();  // 弹出栈顶
            con_id[y] = con_cnt;  // 记录节点y所属的分量编号
            con_size[con_cnt]++;  // 更新分量大小
            ans[con_cnt].push_back(y);  // 将节点加入分量
            if (y == x)  // 直到弹出x为止
                break;
        }
    }
}

int main()
{
    cin >> n >> m;  // 输入节点数和边数
    memset(h, -1, sizeof(h));  // 初始化邻接表

    while (m--)  // 读入所有无向边
    {
        int a, b;
        cin >> a >> b;
        add(a, b), add(b, a);  // 添加无向边
    }

    for (int i = 1; i <= n; i++)  // 遍历所有节点
        if (dfn[i] == 0)  // 如果节点未访问
            tarjan(i, 0);  // 执行Tarjan算法

    // 统计每个边双连通分量的度数
    for (int i = 0; i < idx; i++)  // 遍历所有边
        if (bri[i])  // 如果该边是桥
            d[con_id[e[i]]]++;  // 增加该边连接的分量的度数

    int cnt = 0;  // 统计度数为1的分量数量
    for (int i = 1; i <= con_cnt; i++)  // 遍历所有边双连通分量
        if (d[i] == 1)  // 如果分量的度数为1
            cnt++;  // 计数加1

    cout << (cnt + 1) / 2 << endl;  // 输出需要添加的边数
    return 0;
}

【运行结果】

7 7
1 2
2 3
3 4
2 5
4 5
5 6
5 7
2
posted @ 2026-06-16 10:15  团爸讲算法  阅读(11)  评论(0)    收藏  举报