787. K 站中转内最便宜的航班s
787. K 站中转内最便宜的航班
Category | Difficulty | Likes | Dislikes |
---|---|---|---|
algorithms | Medium (38.84%) | 469 | - |
有 n
个城市通过一些航班连接。给你一个数组 flights
,其中 flights[i] = [fromi, toi, pricei]
,表示该航班都从城市 fromi
开始,以价格 pricei
抵达 toi
。
现在给定所有的城市和航班,以及出发城市 src
和目的地 dst
,你的任务是找到出一条最多经过 k
站中转的路线,使得从 src
到 dst
的 价格最便宜 ,并返回该价格。 如果不存在这样的路线,则输出 -1
。
示例 1:
输入:
n = 3, edges = [[0,1,100],[1,2,100],[0,2,500]]
src = 0, dst = 2, k = 1
输出: 200
解释:
城市航班图如下
从城市 0 到城市 2 在 1 站中转以内的最便宜价格是 200,如图中红色所示。
示例 2:
输入:
n = 3, edges = [[0,1,100],[1,2,100],[0,2,500]]
src = 0, dst = 2, k = 0
输出: 500
解释:
城市航班图如下
从城市 0 到城市 2 在 0 站中转以内的最便宜价格是 500,如图中蓝色所示。
提示:
1 <= n <= 100
0 <= flights.length <= (n * (n - 1) / 2)
flights[i].length == 3
0 <= fromi, toi < n
fromi != toi
1 <= pricei <= 104
- 航班没有重复,且不存在自环
0 <= src, dst, k < n
src != dst
函数签名:
func findCheapestPrice(n int, flights [][]int, src int, dst int, k int) int
分析
最容易想到的是BFS和DFS,根据DFS也可以反推出DP解法。
首先为了能迅速获知某个节点的后续节点,需要把 flights 转化成邻接表。
type Pair struct {
ID, Price int
}
func findCheapestPrice(n int, flights [][]int, src int, dst int, k int) int {
nexts := make([][]Pair, n)
for _, v := range flights {
nexts[v[0]] = append(nexts[v[0]], Pair{ID: v[1], Price: v[2]})
}
// ...
}
BFS
如果没有k的限制,BFS 会很容易,有 k 的限制实际上也并不难,只需要维护BFS的层数并与k判断即可。限制 k 个中转站,即限制 k+1 条边,即限制BFS的层数为 k+1。
type Pair struct {
ID, Price int
}
// inf 既要能表示无限大,又要能在累加的过程中不越界
// 根据数据约束,k的限制为 101,每次航班花费最大 10000, 从起点到终点的最大花费就是 101*10000
const inf = 1010001
func findCheapestPrice(n int, flights [][]int, src int, dst int, k int) int {
nexts := make([][]Pair, n)
for _, v := range flights {
nexts[v[0]] = append(nexts[v[0]], Pair{ID: v[1], Price: v[2]})
}
// memo[x] 表示从src走到x节点花费的最小金额
memo := make([]int, n)
for i := range memo {
memo[i] = inf
}
queue := []Pair{{src, 0}}
memo[src] = 0
res := inf
// steps 是 BFS 的层数,限制最多 k+1 次
for steps := 0; steps <= k && len(queue) > 0; steps++ {
for size := len(queue); size > 0; size-- {
cur := queue[0]
queue = queue[1:]
for _, v := range nexts[cur.ID] {
price := cur.Price + v.Price
if v.ID == dst {
// 不要直接返回,可能有多条路径到达 dst 节点
res = min(res, price)
continue
}
if price > res || memo[v.ID] <= price {
continue
}
queue = append(queue, Pair{v.ID, price})
memo[v.ID] = price
}
}
}
if res == inf {
return -1
}
return res
}
DFS
实际上,可以定义一个递归函数 help 来计算从一个指定节点 start 到达最终 dst 节点的最小花费,除了 start,还需有一个参数 k 来携带步数限制信息。
因为有较多重复计算,带上备忘录优化。
type Pair struct {
ID, Price int
}
const inf = 1010001
func findCheapestPrice(n int, flights [][]int, src int, dst int, k int) int {
nexts := make([][]Pair, n)
for _, v := range flights {
nexts[v[0]] = append(nexts[v[0]], Pair{ID: v[1], Price: v[2]})
}
// help 函数的备忘录
memo := make([][]int, n)
for i := range memo {
memo[i] = make([]int, k+2)
}
// 返回从 start 节点到 dst 节点,限制步数为 k 时的最少花费
var help func(start, k int) int
help = func(start, k int) int {
if k < 0 {
return inf
}
if start == dst {
return 0
}
if memo[start][k] != 0 {
return memo[start][k]
}
res := inf
for _, v := range nexts[start] {
res = min(res, v.Price+help(v.ID, k-1))
}
memo[start][k] = res
return res
}
res := help(src, k+1)
if res == inf {
return -1
}
return res
}
动态规划
从自顶向下的 dfs 解法,容易想到自底向上的动态规划解法。
定义 dp, dp[k][end]
表示经过 k 次航行,到达城市 end 需要的最小花费。
const inf = 1010001
func findCheapestPrice(n int, flights [][]int, src int, dst int, k int) int {
dp := make([][]int, k+2)
for i := range dp {
dp[i] = make([]int, n)
for j := range dp[i] {
dp[i][j] = inf
}
}
dp[0][src] = 0 // dp[*][src] 都应为0,不过这里只需初始化dp[0][src]
// 注意 k 的遍历在外层,航班信息遍历在内层,不能反——为什么?
for t := 1; t <= k+1; t++ {
for _, v := range flights {
i, j, cost := v[0], v[1], v[2]
dp[t][j] = min(dp[t][j], dp[t-1][i]+cost)
}
}
res := inf
for t := 1; t <= k+1; t++ {
res = min(res, dp[t][dst])
}
if res == inf {
res = -1
}
return res
}
比起BFS 和 DFS,代码最简洁,无须预处理 flights。另外 dp 数组可以优化为两个一维数组。
实际这就是 Bellman Ford
算法。