题目描述

✅ 787. K 站中转内最便宜的航班

image-20260928224805606

image-20260928224805610

image-20260928224805613

image-20260928224805617

image-20260928224805618

题意分析

航班是带费用的有向边。最多经过 k 个中转城市,意味着从起点到终点最多乘坐 k + 1 段航班;直飞没有中转,但也使用了一条边。

除了费用,还要考虑航段数量:到同一城市更便宜的路线可能已经用掉更多航段,不能只按费用把所有路线合并为一个永久确定的状态。可以按允许使用的航段数分层,逐层求最优费用。

解法:Bellman-Ford(限制边数)

核心思路

[!blue]

设第 t 轮开始前的 dist[v] 表示从 src 出发、最多使用 t 条边到达城市 v 的最低费用。初始 t = 0,只有起点能以零费用到达,其他位置设为不可达的 INF。

计算下一层时,先把 dist 复制到 newDist,保留已经使用不超过 t 条边的路线。随后遍历每条航班 u -> v,若旧层的 u 可达,就用 dist[u] + price 尝试更新 newDist[v],表示在一条最多 t 段的路线后再接上一段航班。

这个转移覆盖了所有情况:一条最多 t + 1 段的路线,或者已经属于旧层,或者可以拆成到达某个 u 的前缀和最后一条 u -> v。前缀最多使用 t 段,已由旧层求得最小费用。反过来,每个更新候选也只是在旧路线后增加一段,不会超过新层的航段限制。

本轮必须只读 dist、只写 newDist。如果直接原地更新,后扫描的航班可能接着使用本轮刚更新的费用,在一轮里连续走过多条边,破坏“一轮只增加一个允许航段”的含义,也会让结果依赖航班的扫描顺序。

扫描完全部航班后才用 newDist 替换旧层。循环从 t = 0 到 k,共执行 k + 1 次,最终得到航段上限内的最低费用。目标仍为 INF 就说明没有合法路线,返回 -1。

解题步骤

  • 起点零费用,其余不可达。
  • 每轮复制旧层。
  • 沿全部边,从旧层可达点更新新层。
  • 轮末替换,最终返回目标值或负一。

k == 0 时仍要执行一轮,允许乘坐一次直飞航班。没有航班或目标无法在限制内到达时,不可达标记会一直保留;从 INF 出发的边必须跳过。题目最多允许 100 段、每段费用不超过 $10^4$,10^9 足以作为大于所有合法费用的标记。

代码实现

class Solution {
    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
        int INF = 1_000_000_000;
        int[] dist = new int[n];

        Arrays.fill(dist, INF);
        dist[src] = 0;

        for (int t = 0; t <= k; t++) {
            // 保留较少航段的方案,本轮读取旧层、写入新层
            int[] newDist = dist.clone();

            for (int[] e : flights) {
                int u = e[0];
                int v = e[1];
                int w = e[2];

                if (dist[u] == INF) {
                    continue;
                }

                // 只从旧层出发增加一条边,不能串联本轮新结果
                newDist[v] = Math.min(newDist[v], dist[u] + w);
            }

            dist = newDist;
        }

        return dist[dst] == INF ? -1 : dist[dst];
    }
}
func findCheapestPrice(n int, flights [][]int, src int, dst int, k int) int {
    const INF = int(1e9)

    dist := make([]int, n)
    for i := range dist {
        dist[i] = INF
    }
    dist[src] = 0

    for t := 0; t <= k; t++ {
        newDist := make([]int, n)
        // 保留较少航段的方案,本轮读取旧层、写入新层
        copy(newDist, dist)

        for _, e := range flights {
            u, v, w := e[0], e[1], e[2]
            if dist[u] == INF {
                continue
            }
            // 只从旧层出发增加一条边,不能串联本轮新结果
            if dist[u]+w < newDist[v] {
                newDist[v] = dist[u] + w
            }
        }

        dist = newDist
    }

    if dist[dst] == INF {
        return -1
    }
    return dist[dst]
}

复杂度分析

  • 时间复杂度:$O((k+1)(n+E))$,每轮含复制与扫描边。
  • 空间复杂度:$O(n)$,新旧两层。

关键点总结

[!green]

  • 状态是至多若干条边,不是恰好用满。
  • 同城市不同航段的方案不能直接用单一锁定状态替代。

易错点总结

[!yellow]

  • 原地更新会在一轮串联多条航班。
  • 只执行 k 轮,会少允许一段。
  • 不保留旧距离,会丢失提前抵达的合法方案。

相似题目

题目 难度 关联与区别
743. 网络延迟时间 中等 增加最多中转次数后,同一城市在不同已用步数下不是同一状态,不能只保留一个最低价。
1928. 规定时间内到达终点的最小花费 困难 同样在最优路径中增加资源限制,原题限制时间,本题限制经过的边数。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/71826875
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!