问题来源:leetcode 787。
K 站中转内最便宜的航班
有 n
个城市通过 m
个航班连接。每个航班都从城市 u
开始,以价格 w
抵达 v
。
现在给定所有的城市和航班,以及出发城市 src
和目的地 dst
,你的任务是找到从 src
到 dst
最多经过 k
站中转的最便宜的价格。 如果没有这样的路线,则输出 -1
。
动态规划
优化子结构:设 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示最多经过 i i i 个中转站从起点 s r c src src 到达 j j j 的最小代价:
- 如果最后一步不需要中转,那么 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j] 必是最多经过 i − 1 i-1 i−1 个中转站从 s r c src src 到达 j j j 的最小代价,也就是说只需要求解子问题 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j],可以通过反证法来证明:如果最多经过 i − 1 i-1 i−1 个中转站从起点到达 j j j 的最小代价小于 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j],假设为 d p ′ [ i − 1 ] [ j ] dp^{'}[i-1][j] dp′[i−1][j],那么最多经过 i i i 次中转到达 j j j 的代价也就小于 d p [ i ] [ j ] dp[i][j] dp[i][j],与假设矛盾。
- 如果最后一步是从站 k k k 中转过来的,那么 d p [ i ] [ j ] − f l i g h t s [ k ] [ j ] dp[i][j]-flights[k][j] dp[i][j]−flights[k][j] 必是最多经过 i − 1 i-1 i−1 个中转站从起点到达 k k k 的最小代价,也就是说只需要求解子问题 d p [ i − 1 ] [ k ] dp[i-1][k] dp[i−1][k],同样可以使用反证法来证明。
重叠子问题:计算 d p [ 1 ] [ 0 ] dp[1][0] dp[1][0] 时可能用到 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0],计算 d p [ 1 ] [ 1 ] dp[1][1] dp[1][1] 时同样可能用到 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0],所以存在子问题被重复计算。
递归地定义最优解的值:设 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示最多经过 i i i 个中转站从起点 s r c src src 到达 j j j 的最小代价:
- 当 i = 0 i=0 i=0 时, d p [ 0 ] [ j ] = f l i g h t s [ s r c ] [ j ] dp[0][j] = flights[src][j] dp[0][j]=flights[src][j];
- 当 i > 0 i>0 i>0 时, d p [ i ] [ j ] = m i n k { d p [ i − 1 ] [ k ] + f l i g h t s [ k ] [ j ] } dp[i][j]=min_{k}\{\ dp[i-1][k] + flights[k][j] \ \} dp[i][j]=mink{ dp[i−1][k]+flights[k][j] }
自底向上地计算最优解的值:只要在计算 d p dp dp 数组的一行前其前一行数组的值已经被计算完成,那么就可以保证相关的子问题均已经被计算出来。因此可以自左向右、自上而下地填写 d p dp dp 数组。
class Solution {
public:
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
if(src == dst) {
return 0;
}
// 先为航班价格构建索引
unordered_map<int, unordered_map<int, int>> prices;
for(int i=0; i<flights.size(); i++) {
int u = flights[i][0];
int v = flights[i][1];
int price = flights[i][2];
prices[u][v] = price;
}
vector<vector<int>> dp(K+1, vector<int>(n, -1));
dp[0][src] = 0;
for(int i=0; i<n; i++) {
if(prices[src][i] != 0) {
dp[0][i] = prices[src][i];
}
}
for(int i=1; i<=K; i++) {
for(int j=0; j<n; j++) {
dp[i][j] = dp[i-1][j];
for(int k=0; k<n; k++) {
if(dp[i-1][k] != -1 && prices[k][j] != 0) {
dp[i][j] = dp[i][j] != -1 ? min(dp[i][j], dp[i-1][k] + prices[k][j]) : dp[i-1][k] + prices[k][j];
}
}
}
}
return dp[K][dst];
}
};
可以做空间优化,注意到 d p dp dp 数组的每一行仅与它前一行相关,因此可以将 d p dp dp 减少为两行。
class Solution {
public:
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
if(src == dst) {
return 0;
}
// 先为航班价格构建索引
unordered_map<int, unordered_map<int, int>> prices;
for(int i=0; i<flights.size(); i++) {
int u = flights[i][0];
int v = flights[i][1];
int price = flights[i][2];
prices[u][v] = price;
}
int pre = 0, now = 1;
vector<vector<int>> dp(2, vector<int>(n, -1));
dp[0][src] = 0;
for(int i=0; i<n; i++) {
if(prices[src][i] != 0) {
dp[0][i] = prices[src][i];
}
}
for(int i=1; i<=K; i++) {
for(int j=0; j<n; j++) {
dp[now][j] = dp[pre][j];
for(int k=0; k<n; k++) {
if(dp[pre][k] != -1 && prices[k][j] != 0) {
dp[now][j] = dp[now][j] != -1 ? min(dp[now][j], dp[pre][k] + prices[k][j]) : dp[pre][k] + prices[k][j];
}
}
}
swap(pre, now);
}
return dp[pre][dst];
}
};
上面解法的时间复杂度为 O ( K N 2 ) O(KN^2) O(KN2),其中 N N N 为图中顶点数,但注意到没必要填写 d p dp dp 数组的全部位置,在处理每一站时,我们只要看存在的航班即可,这样可以将时间复杂度降为 O ( k E ) O(kE) O(kE),其中 E E E 为图中边数,这在稀疏图中的优化是非常明显的。
class Solution {
public:
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
if(src == dst) {
return 0;
}
int pre = 0, now = 1;
vector<vector<int>> dp(2, vector<int>(n, -1));
dp[0][src] = 0;
for(int j=0; j<flights.size(); j++) {
if(flights[j][0] == src) {
dp[0][flights[j][1]] = flights[j][2];
}
}
for(int i=1; i<=K; i++) {
for(int j=0; j<flights.size(); j++) {
int start = flights[j][0];
int end = flights[j][1];
int cost = flights[j][2];
if(dp[pre][start] != -1) {
dp[now][end] = dp[now][end] == -1 ? dp[pre][start] + cost : min(dp[now][end], dp[pre][start] + cost);
}
}
swap(pre, now);
}
return dp[pre][dst];
}
};