动态规划——K 站中转内最便宜的航班

问题来源:leetcode 787

K 站中转内最便宜的航班

n 个城市通过 m 个航班连接。每个航班都从城市 u 开始,以价格 w 抵达 v

现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到从 srcdst 最多经过 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[i1][j] 必是最多经过 i − 1 i-1 i1 个中转站从 s r c src src 到达 j j j 的最小代价,也就是说只需要求解子问题 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j],可以通过反证法来证明:如果最多经过 i − 1 i-1 i1 个中转站从起点到达 j j j 的最小代价小于 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j],假设为 d p ′ [ i − 1 ] [ j ] dp^{'}[i-1][j] dp[i1][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 i1 个中转站从起点到达 k k k 的最小代价,也就是说只需要求解子问题 d p [ i − 1 ] [ k ] dp[i-1][k] dp[i1][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[i1][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];
    }
};
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值