状态压缩搜索解法(DFS + Dominance)

一、问题回顾

给定一个 M × N 的地图:

  • 0:障碍,不可通过
  • -1:加油站,可将油量重置为 100
  • 正整数:通过该格子的油耗

车辆从左上角 (0,0) 出发,到右下角 (M-1,N-1) 结束,可上下左右移动。

要求计算 保证能到达终点的最小初始油量 ,若无法到达,返回 -1


二、核心观察(关键建模)

1️⃣ 初始油量的真正含义

由于:

  • 加油站会 直接重置油量为 100
  • 油箱最大容量固定为 100

因此,初始油量只需要覆盖"最坏的一段连续消耗"

换句话说:

最小初始油量 = 路径上所有"连续消耗段"的最大值

而不是路径总油耗。


2️⃣ 路径可以拆分为若干"连续消耗段"

  • 遇到 -1:连续段结束,油量重置
  • 正整数:加入当前连续消耗段

问题转化为:

在所有可行路径中,最小化
max(每一段连续消耗)


三、状态设计

我们在 DFS 中维护如下状态:

arduino 复制代码
(y, x, min, used)

含义:

  • (y, x):当前位置
  • used:当前连续消耗段的累计油耗
  • min:到目前为止,路径上出现的 最大连续消耗段(候选答案)

终点处的答案即为:

arduino 复制代码
min === -1 ? used : min

(处理"全程无加油站"的情况)


四、剪枝与状态压缩(核心优化)

1️⃣ 基本可行性剪枝

arduino 复制代码
if (used > 100 || min > 100) return;
if (min > res) return;
  • 超过油箱上限,路径必死
  • 已经不可能优于当前最优解,直接剪枝

2️⃣ Dominance(支配)剪枝 ------ 关键创新点

对每个 (y,x),我们只保留不被支配的状态

定义 dominance 关系:

若存在历史状态 (bMin, bUsed),使得
bMin ≤ minbUsed ≤ used

则当前状态 必然更差,可安全剪枝

实现方式:

ini 复制代码
const [bMin, bUsed] = best[y][x];
if (min !== -1 && min > bMin || min === -1 && used > bMin) return;
if (min === bMin && used >= bUsed) return;
best[y][x] = [min === -1 ? bMin : min, used];

为什么这是安全的?

  • min 是最终优化目标(越小越好)
  • used 只影响后续可行性(越小越好)
  • 被支配状态 在未来不可能产生更优结果

这相当于只保留 Pareto 前沿


五、完整算法流程

  1. (0,0) 开始 DFS
  2. 维护 (min, used) 状态
  3. 遇到加油站重置 used
  4. 使用 dominance 剪枝压缩状态空间
  5. 到达终点更新答案

六、时间复杂度分析

状态规模对比

方法 每个格子状态数 总状态数
Dijkstra(fuel 维度) O(100) O(M·N·100)
本解法(Pareto 前沿) O(1)(极少) O(M·N)

总复杂度

scss 复制代码
O(M × N)

M,N ≤ 200 的限制下,对 JavaScript 非常友好


七、与 Dijkstra 解法的对比

维度 Dijkstra 本解法
状态建模 (x,y,fuel) (x,y,min,used)
状态数 Θ(M·N·100) Θ(M·N)
数据结构 优先队列 DFS + 数组
JS 性能 中等 优秀
通用性 题目特化

本解法利用了"油箱上限 + 加油站重置"的结构性约束,

在该问题下 状态空间严格小于 Dijkstra


八、完整代码(DFS + Dominance)

ini 复制代码
const rl = require("readline").createInterface({ input: process.stdin });
let lines = [];
rl.on("line", l => lines.push(l)).on("close", () => {
    const [M, N] = lines[0].split(',').map(Number);
    let grid = lines.slice(1).map(r => r.split(',').map(Number));

    let res = Infinity;
    let vis = Array.from({ length: M }, () => Array(N).fill(false));
    let best = Array.from({ length: M }, () =>
        Array.from({ length: N }, () => [Infinity, Infinity])
    );

    const dir = [[1,0],[0,1],[0,-1],[-1,0]];

    function dfs(y, x, min, used) {
        if (used > 100 || min > 100 || min > res) return;

        const [bMin, bUsed] = best[y][x];
        if (min !== -1 && min > bMin || min === -1 && used > bMin) return;
        if (min === bMin && used >= bUsed) return;

        best[y][x] = [min === -1 ? bMin : min, used];

        if (y === M - 1 && x === N - 1) {
            res = Math.min(res, min === -1 ? used : min);
            return;
        }

        for (let [dy, dx] of dir) {
            let ny = y + dy, nx = x + dx;
            if (ny < 0 || ny >= M || nx < 0 || nx >= N) continue;
            if (vis[ny][nx] || grid[ny][nx] === 0) continue;

            vis[ny][nx] = true;
            if (grid[ny][nx] === -1) {
                dfs(ny, nx, min === -1 ? used : min, 0);
            } else {
                dfs(ny, nx, min, used + grid[ny][nx]);
            }
            vis[ny][nx] = false;
        }
    }

    vis[0][0] = true;
    dfs(0, 0, -1, Math.max(grid[0][0], 0));

    console.log(res === Infinity ? -1 : res);
});

九、总结

  • 这是一次 问题特化下的状态压缩优化
  • 利用 Pareto dominance 将搜索空间压缩到极限
  • 在 JS 环境下 性能严格优于通用 Dijkstra
相关推荐
颜酱1 小时前
差分数组:高效处理数组区间批量更新的核心技巧
javascript·后端·算法
yyy(十一月限定版)1 小时前
图论——最小生成树Kruskal算法
算法·图论
宇木灵1 小时前
C语言基础-十一、递归与分治(完结)
c语言·开发语言·学习·算法
We་ct2 小时前
LeetCode 173. 二叉搜索树迭代器:BSTIterator类 实现与解析
前端·算法·leetcode·typescript
weixin_395448912 小时前
main.c_0222cursor
c语言·前端·算法
Zik----2 小时前
Leetcode27 —— 移除元素(双指针)
数据结构·算法
陆嵩3 小时前
GMRES 方法的数学推导及其算法表示
算法·概率论·arnoldi·gmres·minres·givens·hessenberg
plus4s3 小时前
2月22日(94-96题)
算法
tankeven3 小时前
HJ98 喜欢切数组的红
c++·算法