题目描述

✅ 1368. 使网格图至少有一条有效路径的最小代价

image-20260929080517864

image-20260929080517998

image-20260929080518472

image-20260929080518928

image-20260929080519072

题意分析

网格每格有一个箭头,编号一、二、三、四分别指向右、左、下、上。希望从左上角沿箭头走到右下角,每修改一个格子的箭头花费一,求最少修改次数。

只需存在一条有效路径,不要求它的移动步数最少,也不要求修改后所有格子都能到终点。原箭头可能指向网格外,路径不能越界;到达终点后无需再离开,所以终点自己的箭头不会产生额外费用。

解法:0-1 BFS

核心思路

[!blue]

把每个格子看作图节点,与上下左右的合法邻居连有向边。从当前格沿原箭头方向走,费用为零;选择其他方向就要修改当前格箭头,费用为一。边权由出发格决定,不由落点决定。

这样一条简单路径的边权和就等于需要修改的格子数。图的边权非负,任何带环路线都能去掉重复的一段而不增加费用,所以总存在最优简单路径,每个格子最多离开一次。沿它设置箭头不会要求同一个格子反复改方向,符合每格只能修改一次的条件。

边权只有零和一,可以用双端队列实现最短路。dist[x][y] 表示目前找到的最低修改代价。从距离 d 的状态扩展,零费用邻居仍是 d,放到队首尽快处理;一费用邻居是 d + 1,放到队尾,排在当前更低代价的状态之后。这相当于按相邻距离层处理,不需要堆排序。

不能在首次入队时就固定答案,因为之后可能找到更低费用的路线。只有 nextDist < dist[nx][ny] 时才更新并入队,状态同时保存入队时的距离;出队后若该距离已不是当前 dist,说明它已被更优状态替代,直接跳过。

有效状态按最低代价优先扩展,非负边不能让后处理状态再改进已经确定的较小距离,因此终点有效状态出队时即可返回。严格改善才入队也避免零费用环反复推入同价状态;单格网格在起点出队时就返回零。

初始距离取 m * n 足以大于最优值:一条不重复格子的路径最多有 m * n - 1 条边,每条代价至多一。搜索中只修改距离表,不需要实际改写原箭头。

解题步骤

  1. 将距离表初始化为 m * n,起点设为零并放入双端队列。
  2. 弹出队首状态,若携带距离与当前距离表不符则跳过;若是终点则返回。
  3. 按右、左、下、上的编号顺序枚举四个合法邻居,根据出发格箭头计算零或一费用。
  4. 只有得到严格更小距离时才更新:零费用状态放队首,一费用状态放队尾。
  5. 持续处理直到终点的有效状态出队。

代码实现

class Solution {
    public int minCost(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int[][] dist = new int[m][n];

        for (int[] row : dist) {
            Arrays.fill(row, m * n);
        }

        // 数组下标加一对应题目的右、左、下、上编号。
        int[][] dirs = {
            {0, 1},
            {0, -1},
            {1, 0},
            {-1, 0},
        };
        Deque<int[]> dq = new ArrayDeque<>();

        dist[0][0] = 0;
        dq.offerFirst(new int[] {
            0,
            0,
            0
        });

        while (!dq.isEmpty()) {
            int[] cur = dq.pollFirst();
            int x = cur[0];
            int y = cur[1];
            int d = cur[2];

            // 已有更短距离替代该队列项,跳过旧状态。
            if (d != dist[x][y]) {
                continue;
            }

            if (x == m - 1 && y == n - 1) {
                return d;
            }

            for (int direction = 0; direction < 4; direction++) {
                int nx = x + dirs[direction][0];
                int ny = y + dirs[direction][1];

                if (nx < 0 || nx >= m || ny < 0 || ny >= n) {
                    continue;
                }

                // 修改费用取决于出发格的箭头,不是落点格。
                int cost = grid[x][y] == direction + 1 ? 0 : 1;
                int nextDist = d + cost;

                // 只有严格改善才入队,避免零费用环重复推送同价状态。
                if (nextDist >= dist[nx][ny]) {
                    continue;
                }

                dist[nx][ny] = nextDist;
                int[] next = {
                    nx,
                    ny,
                    nextDist
                };

                // 零费用放队首,一费用放队尾,使较小距离优先处理。
                if (cost == 0) {
                    dq.offerFirst(next);
                } else {
                    dq.offerLast(next);
                }
            }
        }

        return -1;
    }
}
import "container/list"

func minCost(grid [][]int) int {
    m, n := len(grid), len(grid[0])
    dist := make([][]int, m)
    for i := range dist {
        dist[i] = make([]int, n)
        for j := range dist[i] {
            dist[i][j] = m * n
        }
    }

    // 数组下标加一对应题目的右、左、下、上编号。
    dirs := [4][2]int{
        {0, 1},
        {0, -1},
        {1, 0},
        {-1, 0},
    }
    type state struct {
        x, y, dist int
    }

    dq := list.New()
    dist[0][0] = 0
    dq.PushFront(state{0, 0, 0})

    for dq.Len() > 0 {
        cur := dq.Remove(dq.Front()).(state)
        // 已有更短距离替代该队列项,跳过旧状态。
        if cur.dist != dist[cur.x][cur.y] {
            continue
        }
        if cur.x == m-1 && cur.y == n-1 {
            return cur.dist
        }

        for direction, move := range dirs {
            nx, ny := cur.x+move[0], cur.y+move[1]
            if nx < 0 || nx >= m || ny < 0 || ny >= n {
                continue
            }
            cost := 1
            // 修改费用取决于出发格的箭头,不是落点格。
            if grid[cur.x][cur.y] == direction+1 {
                cost = 0
            }
            nextDist := cur.dist + cost
            // 只有严格改善才入队,避免零费用环重复推送同价状态。
            if nextDist >= dist[nx][ny] {
                continue
            }
            dist[nx][ny] = nextDist
            next := state{nx, ny, nextDist}
            // 零费用放队首,一费用放队尾,使较小距离优先处理。
            if cost == 0 {
                dq.PushFront(next)
            } else {
                dq.PushBack(next)
            }
        }
    }
    return -1
}

复杂度分析

  • 时间复杂度:$O(mn)$,网格每格最多四条合法出边,0-1 BFS 为 $O(V+E)$。
  • 空间复杂度:$O(mn)$,用于距离表和队列状态。

关键点总结

[!green]

  • 一费用边对应修改出发格,最优简单路径让每格至多修改一次,完成题目与最短路的对应。
  • 零费用放队首、一费用放队尾,维持低代价优先的处理顺序。
  • 首次发现不代表最终最短,需要允许严格改善并跳过过期队列项。
  • 方向编号必须与题面一致,左右上下四个方向都要考虑。

易错点总结

[!yellow]

  • 按落点箭头判断费用,会把应由出发格决定的修改算错。
  • 首次入队就永久锁定节点,可能拒绝后续零费用路线的改进。
  • 两种费用都按普通 FIFO 并首次确定距离,无法保证最小代价先到。
  • 相等距离也再次入队,会让零费用环重复制造无意义状态。
  • 非起点距离初始化为零,所有非负候选都无法严格改善,搜索不能展开。
  • 只尝试向右和向下会漏掉需要绕行的低费用路径。

相似题目

题目 难度 关联与区别
1263. 推箱子 困难 两题都可转为0/1边权,本题沿箭头走免费、改方向付1,推箱题走路免费、推箱付1。
743. 网络延迟时间 中等 非负边权都可Dijkstra,但本题只有0和1两种权值,双端队列可进一步简化。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2022/35926189
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!