有一台仓储巡检车,需要在一个 M × N 的网格仓库中,从起点驶向目标点。
仓库中会逐步启用若干“高风险格” ,巡检车可以穿过这些高风险格,但系统会认为这样存在风险。
现在给定网格大小、巡检车的起点与终点,以及若干次高风险格启用操作。
要求在每次启用一个高风险格之后,判断巡检车从起点到终点的最佳路线情况。
路线优先级定义如下:
- 优先让经过的高风险格数量最少
- 若高风险格数量相同,选择总步数最少的路线
- 若仍然相同,选择整条路径字典序最小的路线
(路径按经过坐标序列比较,先比x,再比y)
巡检车每次只能向上、下、左、右移动一格,不能越界。
对于每次启用操作:
- 若存在一条路线完全不经过任何高风险格,输出
CLEAR - 否则输出
RISK,并输出这条最优路线中经过的所有高风险格坐标
说明:起点本身不额外计入风险;是否计入某个格子的风险,以“进入该格子”为准。
输入说明
第一行:两个整数 M N,表示网格大小。
第二行:两个整数 sx sy,表示巡检车起点坐标。
第三行:两个整数 tx ty,表示目标点坐标。
第四行:一个整数 q,表示接下来有 q 次高风险格启用操作。
接下来 q 行:每行两个整数 x y,表示本轮启用的高风险格坐标。
高风险格一旦启用,会在后续所有轮次中持续生效。
输出说明
对于每次启用操作,输出一行结果:
- 若最优路线不经过任何高风险格,输出:
CLEAR
- 否则输出:
RISK k x1 y1 x2 y2 ... xk yk
其中:
k表示该最优路线经过的高风险格数量- 后面按路线经过顺序依次输出这些高风险格坐标
测试用例
输入:
3 3
0 0
2 2
4
0 1
1 0
1 2
2 1
输出:
CLEAR
RISK 1 0 1
RISK 1 1 0
RISK 2 0 1 1 2
参考答案
#include <algorithm>
#include <iostream>
#include <limits>
#include <queue>
#include <tuple>
#include <utility>
#include <vector>
class Solution {
public:
Solution(int m, int n, int sx, int sy, int tx, int ty)
: m_(m),
n_(n),
sx_(sx),
sy_(sy),
tx_(tx),
ty_(ty),
blocked_(m, std::vector<int>(n, 0)) {}
void AddBlocked(int x, int y) {
blocked_[x][y] = 1;
RunOnce();
}
private:
bool InBound(int x, int y) const {
return x >= 0 && x < m_ && y >= 0 && y < n_;
}
long long BigCost() const {
return 1LL * m_ * n_ + 1;
}
long long EnterCost(int x, int y) const {
return blocked_[x][y] ? (BigCost() + 1) : 1;
}
void RunOnce() {
const long long inf = std::numeric_limits<long long>::max() / 4;
std::vector<std::vector<long long>> dist(
m_, std::vector<long long>(n_, inf));
using State = std::tuple<long long, int, int>;
std::priority_queue<State, std::vector<State>, std::greater<State>> pq;
dist[tx_][ty_] = 0;
pq.push({0, tx_, ty_});
const int dx[4] = {-1, 1, 0, 0};
const int dy[4] = {0, 0, -1, 1};
// 反向 Dijkstra:
// dist[x][y] = 从 (x,y) 到终点的最优代价
while (!pq.empty()) {
auto [d, x, y] = pq.top();
pq.pop();
if (d != dist[x][y]) continue;
for (int k = 0; k < 4; ++k) {
int px = x + dx[k];
int py = y + dy[k];
if (!InBound(px, py)) continue;
long long nd = d + EnterCost(x, y);
if (nd < dist[px][py]) {
dist[px][py] = nd;
pq.push({nd, px, py});
}
}
}
long long obstacle_count = dist[sx_][sy_] / BigCost();
if (obstacle_count == 0) {
std::cout << "CLEAR\n";
return;
}
std::vector<std::pair<int, int>> risk_cells;
int x = sx_;
int y = sy_;
// 正向恢复路径:
// 选满足 dist[cur] == EnterCost(next) + dist[next] 的邻居,
// 再按字典序取最小。
while (!(x == tx_ && y == ty_)) {
std::vector<std::pair<int, int>> candidates;
for (int k = 0; k < 4; ++k) {
int nx = x + dx[k];
int ny = y + dy[k];
if (!InBound(nx, ny)) continue;
if (dist[nx][ny] >= inf) continue;
if (dist[x][y] == EnterCost(nx, ny) + dist[nx][ny]) {
candidates.push_back({nx, ny});
}
}
std::sort(candidates.begin(), candidates.end());
auto [nx, ny] = candidates[0];
if (blocked_[nx][ny]) {
risk_cells.push_back({nx, ny});
}
x = nx;
y = ny;
}
std::cout << "RISK " << risk_cells.size();
for (auto [rx, ry] : risk_cells) {
std::cout << " " << rx << " " << ry;
}
std::cout << "\n";
}
private:
int m_;
int n_;
int sx_;
int sy_;
int tx_;
int ty_;
std::vector<std::vector<int>> blocked_;
};
int main() {
int m, n;
std::cin >> m >> n;
int sx, sy;
std::cin >> sx >> sy;
int tx, ty;
std::cin >> tx >> ty;
int q;
std::cin >> q;
Solution sol(m, n, sx, sy, tx, ty);
for (int i = 0; i < q; ++i) {
int x, y;
std::cin >> x >> y;
sol.AddBlocked(x, y);
}
return 0;
}
复盘
一、这道题的本质是什么
这题表面上像“网格搜索”,但本质上不是普通 BFS,而是一个:
多目标最短路 + 路径恢复
因为它的优先级不是单一的“最短步数”,而是:
- 先最少经过障碍
- 再最短路径
- 再字典序最小
所以这题的核心不是“能不能到”,而是:
如何定义最优代价
如何在多个最优解中恢复出题目要求的那一条具体路径
二、这题正确的方法论
这题最后整理出来,其实可以压缩成一个很清晰的方法论。
第一步:先定义状态
这题最重要的一句:
dist[x][y] = 从 (x,y) 到终点的最优代价
第二步:把多目标优先级压成单一代价
题目要求:
- 障碍物最少
- 路径最短
所以可以定义:
进入空地代价 = 1
进入障碍物代价 = BIG + 1
BIG = M * N + 1
这样总代价就是:
障碍物数 * BIG + 步数
就自动满足:
- 先比障碍物数
- 再比步数
第三步:从终点反向跑 Dijkstra
因为我要恢复的是:
从起点走到终点的字典序最小最优路径
如果先求出每个点到终点的最优值,那么恢复路径就会特别方便。
第四步:从起点正向恢复路径
每一步只考虑那些满足:
dist[cur] == EnterCost(next) + dist[next]
的邻居。
它们都是“合法最优下一步”。
然后从这些候选里选字典序最小的那个。
第五步:路径恢复时顺手记录障碍物
一旦走到的 next 是障碍物,就把它记进答案。
三、这题最终的标准解法总结
一句话总结:
反向 Dijkstra 求值,正向贪心恢复路径
这是这题最核心的结构。
四、我这次最大的收获
我这次真正学到的不是“又做了一道网格题”,而是:
当题目要求“最优值 + 具体路径”时,不能只会求最短路,还要会恢复方案。
而且以后遇到复杂 tie-break 的题,我更应该想到:
值和路径分离
先算最优值
再根据最优值恢复路径
这个思路比一开始就在最短路里硬维护所有路径信息要稳得多。
方法论总结
以后再遇到这类题,我要按下面这个模板去写。
模板 1:先写定义,不急着写代码
先在草稿纸上写:
状态定义
dist[...] 表示什么?
代价定义
cost(...) 表示什么?
转移条件
怎么从一个状态更新另一个状态?
恢复条件
如何判断下一步仍然最优?
如果这几句没写清楚,不要急着敲代码。
模板 2:先分阶段,再分函数
对于“最优值 + 路径”的题,优先考虑拆成:
第一阶段
只求最优值
第二阶段
只恢复路径
第三阶段
统一输出
模板 3:每写一段都问自己“不变量是什么”
比如这题里:
在 Dijkstra 中
dist[x][y] 始终表示从 (x,y) 到终点的当前最优代价
在恢复路径中
只有满足 dist[cur] == EnterCost(next) + dist[next] 的 next 才合法
这样代码就不会靠猜。
模板 4:输出逻辑最后再写
不要边搜边输出。
先把答案算完,放在变量里,最后再统一 print。
下次我应该怎么写题目
如果下次再遇到这种题,我应该按这个顺序来:
第 1 步:先读题并提炼优先级
先把题目最重要的比较规则写出来:
- 第一目标是什么
- 第二目标是什么
- 第三目标是什么
不要一上来就敲代码。
第 2 步:判断这是不是“普通 BFS 题”
如果每一步代价都一样,用 BFS。
如果有“不同代价”或者“多目标优先级”,就往 Dijkstra / DP / 分层图去想。
第 3 步:先写状态和代价
这一步一定要落成文字:
dist 的含义
cost 的含义
转移公式
第 4 步:先实现“求值”
别想着一口气把路径恢复也写完。
先做到:
- 程序能正确算出最优值
- 能正确判断 OK/BLOCK
第 5 步:再实现“恢复路径”
这一步只关心:
- 下一步合法条件是什么
- 多个合法候选怎么比较
第 6 步:最后补输出格式
确保和题目要求完全一致。
最后一句总结
这题对我来说,真正的提升不是“学会了一个 Dijkstra 变形”,而是:
我开始意识到:做题不能只会想算法,还要会把算法拆成“定义 → 求值 → 恢复 → 输出”的代码结构。
以后只要是:
- 多目标最优
- 要输出具体方案
- 代码容易越写越长
我都要提醒自己:
不要边写边猜,要先定定义,再按定义写。