【算法】巡检车路径评估

3 阅读7分钟

有一台仓储巡检车,需要在一个 M × N 的网格仓库中,从起点驶向目标点。
仓库中会逐步启用若干“高风险格” ,巡检车可以穿过这些高风险格,但系统会认为这样存在风险。

现在给定网格大小、巡检车的起点与终点,以及若干次高风险格启用操作。
要求在每次启用一个高风险格之后,判断巡检车从起点到终点的最佳路线情况。

路线优先级定义如下:

  1. 优先让经过的高风险格数量最少
  2. 若高风险格数量相同,选择总步数最少的路线
  3. 若仍然相同,选择整条路径字典序最小的路线
    (路径按经过坐标序列比较,先比 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,而是一个:

多目标最短路 + 路径恢复

因为它的优先级不是单一的“最短步数”,而是:

  1. 先最少经过障碍
  2. 再最短路径
  3. 再字典序最小

所以这题的核心不是“能不能到”,而是:

如何定义最优代价
如何在多个最优解中恢复出题目要求的那一条具体路径


二、这题正确的方法论

这题最后整理出来,其实可以压缩成一个很清晰的方法论。


第一步:先定义状态

这题最重要的一句:

dist[x][y] = 从 (x,y) 到终点的最优代价

第二步:把多目标优先级压成单一代价

题目要求:

  1. 障碍物最少
  2. 路径最短

所以可以定义:

进入空地代价 = 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 变形”,而是:

我开始意识到:做题不能只会想算法,还要会把算法拆成“定义 → 求值 → 恢复 → 输出”的代码结构。

以后只要是:

  • 多目标最优
  • 要输出具体方案
  • 代码容易越写越长

我都要提醒自己:

不要边写边猜,要先定定义,再按定义写。