题解:洛谷 P6066 Watchcow

【题目来源】

洛谷:P6066 [USACO05JAN] Watchcow S - 洛谷

【题目描述】

Farmer John 有 \(N\) 个农场(\(2 \leq N \leq 10^4\)),这些农场由 \(M\) 条道路连接(\(1 \leq M \leq 5 \times 10^4\))。不保证没有重边。

Bassie 从 \(1\) 号农场开始巡逻,每条路必须从两个方向各走恰好一遍,最后回到 \(1\) 号农场。

请输出一条满足上述要求的路径。

保证这样的路径存在。如果有多条路径,任意输出一条即可。

【输入】

第一行两个整数 \(N,M\)

接下来 \(M\) 行,每行两个整数 \(u,v\),描述一条 \(u\)\(v\) 的道路。

【输出】

输出经过的农场,一行一个。

【输入样例】

4 5
1 2
1 4
2 3
2 4
3 4

【输出样例】

1
2
3
4
2
1
4
3
2
4
1

【核心思想】

  1. 问题分析:给定 \(N\) 个农场、\(M\) 条无向边(可能有重边),要求从农场 \(1\) 出发,每条边从两个方向各走恰好一遍,最后回到 \(1\)。输出一条满足条件的路径。这是一个欧拉回路(双向遍历版本)问题,关键在于将无向边的每条边视为两条有向边,求每条有向边恰好经过一次的欧拉回路。

  2. 算法选择

    • DFS 找欧拉回路(Hierholzer 算法):通过深度优先搜索,在回溯时将节点加入路径,得到欧拉回路
    • 边删除标记:访问一条边后将其终点标记为 \(0\)(相当于删除该有向边),避免重复访问
    • 后序输出:欧拉回路的路径在 DFS 回溯时记录,需逆序(或后序)输出
  3. 关键步骤

    • 建图:读入 \(N, M\)\(M\) 条边。对每条无向边 \((u, v)\),添加两条有向边 \(u \to v\)\(v \to u\)
    • DFS 遍历(从节点 \(1\) 开始):
      • 遍历当前节点 \(u\) 的所有邻接边
      • 对每条未访问的边(终点不为 \(0\)):
        • 标记该边为已访问(e[i] = 0
        • 递归访问邻接节点 dfs(j)
      • 回溯时将 \(u\) 加入队列 q.push(u)
    • 输出路径:依次输出队列中的节点
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N + M)\),每条边被访问恰好一次
    • 空间复杂度:\(O(N + M)\),邻接表存储和队列
  5. DFS 欧拉回路的核心思想

    • 有向边建模:将无向边的"双向各走一遍"转化为两条独立的有向边,问题变为求每条有向边恰好经过一次的欧拉回路
    • 边删除保证唯一性:访问后将边标记为 \(0\),确保每条有向边只被遍历一次
    • 后序记录路径:DFS 回溯时才将节点加入路径,这样得到的是从叶子回到根的顺序,恰好是欧拉回路的正确顺序
    • Hierholzer 算法的递归实现:该算法核心思想是"尽可能走,走不通时回溯并记录",保证每条边都被覆盖且路径连续
    • 存在性保证:题目保证路径存在,即图是欧拉图(所有节点度数为偶数,且连通)
    • 适用于"每条边双向遍历一次的欧拉回路"问题,核心在于有向边建模和 DFS 边删除策略

【解题思路】

【算法标签】

普及+ #DFS-图

【代码详解】

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

// 定义常量
const int N = 10005;        // 最大顶点数
const int M = 50005 * 2;    // 最大边数,无向图需要乘以2

int n, m;                   // n: 顶点数, m: 边数
int h[N];                   // 邻接表头数组,h[u]表示顶点u的第一条边的索引
int e[M];                   // 边数组,e[i]表示第i条边的终点顶点
int ne[M];                  // 邻接表next数组,ne[i]表示下一条边的索引
int idx;                    // 边的索引计数器
queue<int> q;               // 队列,用于存储DFS后序遍历的顶点

// 添加边的函数
void add(int a, int b)
{
    e[idx] = b;             // 记录边的终点
    ne[idx] = h[a];         // 新边指向原来链表的头
    h[a] = idx++;           // 更新链表头,并递增索引
}

// 深度优先搜索函数
void dfs(int u)
{
    // 遍历顶点u的所有邻接边
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];       // 获取邻接顶点
        if (j)              // 如果邻接顶点存在
        {
            e[i] = 0;       // 标记当前边为已访问
            dfs(j);         // 递归访问邻接顶点
        }
    }
    q.push(u);              // 后序遍历:在访问完所有邻接顶点后,将当前顶点加入队列
}

int main()
{
    // 初始化邻接表头数组为-1
    memset(h, -1, sizeof(h));

    // 读入顶点数和边数
    cin >> n >> m;

    // 读入每条边
    for (int i = 1; i <= m; i++)
    {
        int u, v;
        cin >> u >> v;
        add(u, v);  // 添加u到v的边
        add(v, u);  // 添加v到u的边(无向图需要双向添加)
    }

    // 从顶点1开始深度优先搜索
    dfs(1);

    // 输出DFS后序遍历的结果
    while (!q.empty())
    {
        cout << q.front() << endl;
        q.pop();
    }

    return 0;
}

【运行结果】

4 5
1 2
1 4
2 3
2 4
3 4
1
2
3
2
1
4
2
4
3
4
1
posted @ 2026-08-11 11:27  团爸讲算法  阅读(9)  评论(0)    收藏  举报