题解:洛谷 P1849 Tractor

【题目来源】

洛谷:P1849 [USACO12MAR] Tractor S - 洛谷

【题目描述】

经过一天漫长的工作,农场主 John 完全忘记了他的拖拉机还在场地中央。他的奶牛们总喜欢和他搞些恶作剧,它们在场地的不同位置丢下 \(n\) 堆干草。这样 John 就必须先移走一些干草堆才能将拖拉机开走。

拖拉机和干草堆都可以看作是二维平面上的点,它们的坐标都是整数,没有哪堆干草的坐标和拖拉机的初始坐标一致。John 驾驶拖拉机只能沿着坐标轴的方向移动若干单位长度,比如说,他可以先朝北移动 \(2\) 个单位长度,再向东移动 \(3\) 个单位长度等等。拖拉机不能移动到干草堆所占据的点。

请你帮助 John 计算一下,最少要移动多少堆干草才能将拖拉机开回坐标原点。

【输入】

输入的第一行是三个用空格隔开的整数,依次代表干草的堆数 \(n\) 和拖拉机的起始坐标 \((x_0, y_0)\)

\(2\) 行到第 \((n+1)\) 行,每行有两个用空格隔开的整数,第 \((i + 1)\) 行的整数 \(x_i, y_i\) 代表第 \(i\) 堆干草的坐标为 \((x_i, y_i)\)

【输出】

一行一个整数,表示最少要移动多少堆干草 John 才能将拖拉机开回坐标原点。

【输入样例】

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

【输出样例】

1

【核心思想】

  1. 问题分析:给定拖拉机起始坐标 \((x_0, y_0)\)\(n\) 堆干草坐标,拖拉机只能沿坐标轴方向移动,不能经过干草堆。求将拖拉机开回原点 \((0, 0)\) 最少需要移走多少堆干草。这是一个01-BFS(边权为0/1的最短路)问题,关键在于将"经过干草堆需要移走"建模为边权 \(1\),普通移动边权 \(0\),求到原点的最小代价。

  2. 算法选择

    • 01-BFS(双端队列BFS):网格中移动到非干草堆位置代价为 \(0\),移动到干草堆位置代价为 \(1\)(需要移走)。用双端队列实现 \(O(1)\) 处理 \(0\) 权边、\(O(1)\) 处理 \(1\) 权边
    • 坐标离散化/网格化:由于坐标范围有限(\(0 \leq x, y \leq 1000\)),直接在网格上运行BFS
  3. 关键步骤

    • 初始化网格a[x][y] = 1 表示该位置有干草堆(代价 \(1\)),否则为 \(0\)(代价 \(0\)
    • 01-BFS(从 \((x_0, y_0)\) 开始):
      • f[x][y] 表示到达 \((x, y)\) 需要移走的最少干草堆数
      • 初始化 f\(\infty\)f[x_0][y_0] = 0
      • 使用双端队列:
        • 移动到非干草堆(代价 \(0\)):从队首入队
        • 移动到干草堆(代价 \(1\)):从队尾入队
      • 标准BFS扩展四邻域,按边权选择入队位置
    • 输出答案f[0][0]
  4. 时间/空间复杂度

    • 时间复杂度:\(O(XY)\)\(X, Y\) 为坐标范围(本题中约 \(1000 \times 1000\)
    • 空间复杂度:\(O(XY)\)f 数组和访问标记
  5. 01-BFS 的核心思想

    • 代价建模:将"移走干草堆"这一操作转化为移动到该格子的代价 \(1\),普通移动代价 \(0\),问题转化为求到原点的最小代价路径
    • 双端队列优化:边权只有 \(0\)\(1\) 时,用双端队列替代Dijkstra的优先队列,\(0\) 权边从队首扩展(保证先处理)、\(1\) 权边从队尾扩展,实现 \(O(V+E)\) 时间复杂度
    • 网格化直接处理:坐标范围较小,无需离散化,直接在二维网格上BFS
    • 最短路即最少移除:到达原点的最小代价等于路径上经过的干草堆数量,即最少需要移走的干草堆数
    • 适用于"网格移动有代价、求最小代价路径"的问题,特别是边权仅为0/1的场景

【算法标签】

普及+ #BFS-二维

【代码详解】

#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;  // 定义坐标对类型
const int N = 1005;  // 最大网格尺寸
int n;  // 输入的火把数量
int a[N][N], f[N][N];  // a: 标记危险位置,f: 从起点到每个点的最短路径长度
int dx[4] = {-1, 1, 0, 0};  // 方向数组:上下左右
int dy[4] = {0, 0, -1, 1};  // 方向数组:上下左右

/**
 * 广度优先搜索计算最短路径
 * @param x 起始点x坐标
 * @param y 起始点y坐标
 */
void bfs(int x, int y)
{
    memset(f, 0x3f, sizeof(f));  // 初始化距离为无穷大
    f[x][y] = 0;  // 起点距离为0
    queue<PII> q;  // 定义队列
    q.push({x, y});  // 将起点加入队列

    while (!q.empty())  // 当队列不为空时
    {
        int xx = q.front().first, yy = q.front().second;  // 取出队头坐标
        q.pop();  // 弹出队头

        for (int i = 0; i < 4; i++)  // 遍历四个方向
        {
            int nx = xx + dx[i], ny = yy + dy[i];  // 计算相邻位置

            if (nx < 0 || nx > 1001 || ny < 0 || ny > 1001)  // 越界检查
                continue;

            // 如果通过当前点到达相邻点距离更短
            if (f[nx][ny] > f[xx][yy] + a[nx][ny])
            {
                f[nx][ny] = f[xx][yy] + a[nx][ny];  // 更新最短距离
                q.push({nx, ny});  // 将新位置加入队列
            }
            // 调试输出
            // cout << "nx ny a[nx][ny] " << nx << " " << ny << " " << a[nx][ny] << endl;
            // cout << "f[] " << f[nx][ny] << endl;
        }
    }
}
int main()
{
    cin >> n;  // 输入火把数量
    int x0, y0;
    cin >> x0 >> y0;  // 输入起始坐标

    // 标记火把位置
    for (int i = 1; i <= n; i++)
    {
        int x, y;
        cin >> x >> y;  // 输入火把坐标
        a[x][y] = 1;  // 标记该位置为危险位置
    }

    bfs(x0, y0);  // 计算从起点到所有位置的最短路径

    // 调试输出
    // for (int i=0; i<=8; i++)
    // {
    //     for (int j=0; j<=8; j++)
    //         cout << f[i][j] << " ";
    //     cout << endl;
    // }

    cout << f[0][0] << endl;  // 输出到达(0,0)的最短路径长度
    return 0;
}

【运行结果】

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