题解:洛谷 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
【核心思想】
-
问题分析:给定拖拉机起始坐标 \((x_0, y_0)\) 和 \(n\) 堆干草坐标,拖拉机只能沿坐标轴方向移动,不能经过干草堆。求将拖拉机开回原点 \((0, 0)\) 最少需要移走多少堆干草。这是一个01-BFS(边权为0/1的最短路)问题,关键在于将"经过干草堆需要移走"建模为边权 \(1\),普通移动边权 \(0\),求到原点的最小代价。
-
算法选择:
- 01-BFS(双端队列BFS):网格中移动到非干草堆位置代价为 \(0\),移动到干草堆位置代价为 \(1\)(需要移走)。用双端队列实现 \(O(1)\) 处理 \(0\) 权边、\(O(1)\) 处理 \(1\) 权边
- 坐标离散化/网格化:由于坐标范围有限(\(0 \leq x, y \leq 1000\)),直接在网格上运行BFS
-
关键步骤:
- 初始化网格:
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]
- 初始化网格:
-
时间/空间复杂度:
- 时间复杂度:\(O(XY)\),\(X, Y\) 为坐标范围(本题中约 \(1000 \times 1000\))
- 空间复杂度:\(O(XY)\),
f数组和访问标记
-
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
浙公网安备 33010602011771号