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

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

现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到从 srcdst 最多经过 k 站中转的最便宜的价格。 如果没有这样的路线,则输出 -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,如图中蓝色所示。

提示:

  • n 范围是 [1, 100],城市标签从 0n`` - 1.
  • 航班数量范围是 [0, n * (n - 1) / 2].
  • 每个航班的格式 (src, ``dst``, price).
  • 每个航班的价格范围是 [1, 10000].
  • k 范围是 [0, n - 1].
  • 航班没有重复,且不存在环路

首先我们要建立这个图,选取的数据结构就是邻接链表的形式,具体来说就是建立每个结点和其所有能到达的结点的集合之间的映射,然后就是用DFS来遍历这个图了,用变量cur表示当前遍历到的结点序号,还是当前剩余的转机次数K,访问过的结点集合visited,当前累计的价格out,已经全局的最便宜价格res。在递归函数中,首先判断如果当前cur为目标结点dst,那么结果res赋值为out,并直接返回。你可能会纳闷为啥不是取二者中较小值更新结果res,而是直接赋值呢?原因是我们之后做了剪枝处理,使得out一定会小于结果res。然后判断如果K小于0,说明超过转机次数了,直接返回。然后就是遍历当前结点cur能到达的所有结点了,对于遍历到的结点,首先判断如果当前结点已经访问过了,直接跳过。或者是当前价格out加上到达这个结点需要的价格之和大于结果res的话,那么直接跳过。这个剪枝能极大的提高效率,是压线过OJ的首要功臣。之后就是标记结点访问,调用递归函数,以及还原结点状态的常规操作了,参见代码如下:

解法一:

class Solution {
public:
    int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
        int res = INT_MAX;
        unordered_map<int, vector<vector<int>>> m;
        unordered_set<int> visited{{src}};
        for (auto flight : flights) {
            m[flight[0]].push_back({flight[1], flight[2]});
        }
        helper(m, src, dst, K, visited, 0, res);
        return (res == INT_MAX) ? -1 : res;
    }
    void helper(unordered_map<int, vector<vector<int>>>& m, int cur, int dst, int K, unordered_set<int>& visited, int out, int& res) {
        if (cur == dst) {res = out; return;}
        if (K < 0) return;
        for (auto a : m[cur]) {
            if (visited.count(a[0]) || out + a[1] > res) continue;
            visited.insert(a[0]);
            helper(m, a[0], dst, K - 1, visited, out + a[1], res);
            visited.erase(a[0]);
        }
    }
};

下面这种解法是用BFS来做的,还是来遍历图,不过这次是一层一层的遍历,需要使用queue来辅助。前面建立图的数据结构的操作和之前相同,BFS的写法还是经典的写法,但需要注意的是这里也同样的做了剪枝优化,当当前价格加上新到达位置的价格之和大于结果res的话直接跳过。最后注意如果超过了转机次数就直接break,参见代码如下:

解法二:

class Solution {
public:
    int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
        int res = INT_MAX, cnt = 0;
        unordered_map<int, vector<vector<int>>> m;
        queue<vector<int>> q{{{src, 0}}};
        for (auto flight : flights) {
            m[flight[0]].push_back({flight[1], flight[2]});
        }
        while (!q.empty()) {
            for (int i = q.size(); i > 0; --i) {
                auto t = q.front(); q.pop();
                if (t[0] == dst) res = min(res, t[1]);
                for (auto a : m[t[0]]) {
                    if (t[1] + a[1] > res) continue;
                    q.push({a[0], t[1] + a[1]});
                }
            }
            if (cnt++ > K) break;
        }
        return (res == INT_MAX) ? -1 : res;
    }
};

再来看使用Bellman Ford算法的解法,关于此算法的detail可以上网搜帖子看看。核心思想还是用的动态规划Dynamic Programming,最核心的部分就是松弛操作Relaxation,也就是DP的状态转移方程。这里我们使用一个二维DP数组,其中dp[i][j]表示最多飞i次航班到达j位置时的最少价格,那么dp[0][src]初始化为0,因为飞0次航班的价格都为0,转机K次,其实就是飞K+1次航班,我们开始遍历,i从1到K+1,每次dp[i][src]都初始化为0,因为在起点的价格也为0,然后即使遍历所有的航班x,更新dp[i][x[1]],表示最多飞i次航班到达航班x的目的地的最低价格,用最多飞i-1次航班,到达航班x的起点的价格加上航班x的价格之和,二者中取较小值更新即可,参见代码如下:

解法三:

class Solution {
public:
    int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
        vector<vector<int>> dp(K + 2, vector<int>(n, 1e9));
        dp[0][src] = 0;
        for (int i = 1; i <= K + 1; ++i) {
            dp[i][src] = 0;
            for (auto x : flights) {
                dp[i][x[1]] = min(dp[i][x[1]], dp[i - 1][x[0]] + x[2]);
            }
        }
        return (dp[K + 1][dst] >= 1e9) ? -1 : dp[K + 1][dst];
    }
};

我们可以稍稍优化下上面解法的空间复杂度,使用一个一维的DP数组即可,具体思路没有啥太大的区别,参见代码如下:

解法四:

class Solution {
public:
    int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
        vector<int> dp(n, 1e9);
        dp[src] = 0;
        for (int i = 0; i <= K; ++i) {
            vector<int> t = dp;
            for (auto x : flights) {
                t[x[1]] = min(t[x[1]], dp[x[0]] + x[2]);
            }
            dp = t;
        }
        return (dp[dst] >= 1e9) ? -1 : dp[dst];
    }
};

方法一:计算到各节点的最短距离【通过】

思路和算法

假设 pre[node] 是到经过 T 个节点到达目的节点 node 的最短距离,然后求解经过 T+1 个节点到达目的节点的最短距离。对于每一条连接城市 u 和 v,成本为 w的航线,更新后的最短距离为 dis[v] = min(dis[v], pre[u] + w)。

实际上,初始令 dis = dist[0] 和 pre = dist[1],在下一步循环迭代 (i = 1) 时,可以重复使用 dis = dist[1] 和 pre = dist[0],以此类推。

java

class Solution {
    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int K) {
        int[][] dist = new int[2][n];
        int INF = Integer.MAX_VALUE / 2;
        Arrays.fill(dist[0], INF);
        Arrays.fill(dist[1], INF);
        dist[0][src] = dist[1][src] = 0;

        for (int i = 0; i <= K; ++i)
            for (int[] edge: flights)
                dist[i&1][edge[1]] = Math.min(dist[i&1][edge[1]], dist[~i&1][edge[0]] + edge[2]);

        return dist[K&1][dst] < INF ? dist[K&1][dst] : -1;
    }
}

python

class Solution(object):
    def findCheapestPrice(self, n, flights, src, dst, K):
        dist = [[float('inf')] * n for _ in xrange(2)]
        dist[0][src] = dist[1][src] = 0

        for i in xrange(K+1):
            for u, v, w in flights:
                dist[i&1][v] = min(dist[i&1][v], dist[~i&1][u] + w)

        return dist[K&1][dst] if dist[K&1][dst] < float('inf') else -1

复杂度分析

时间复杂度:O(E∗K),其中 E 是 flights 的长度。

空间复杂度:O(n),存储 dis 和 pre。

方法二:Dijkstra【通过】

思路

寻找源到目标的最低花费,Dijkstra 是一个好的算法。

Dijstra 算法的基本思想就是:按照 cost 从小到大的顺序扩展所有可能的飞行路线,当城市被添加到 dst 时,dst 中对应的值就是到达该城市的最低花费。

算法

在 Dijkstra 算法中,借助优先级队列持续搜索花费最低的下一个城市。

如果查找到某个城市,它原本的路线成本更低或者中转次数过多,则无需再搜索它。否则,如果搜索到目的城市,那么当前花费就是最低成本,因为每次最先搜索的就是最低成本航线。

否则,如果从 node 城市出发的航线花费更低,则将该节点加入到优先级队列用于搜索。

java

class Solution {
    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int K) {
        int[][] graph = new int[n][n];
        for (int[] flight: flights)
            graph[flight[0]][flight[1]] = flight[2];

        Map<Integer, Integer> best = new HashMap();

        PriorityQueue<int[]> pq = new PriorityQueue<int[]>((a, b) -> a[0] - b[0]);
        pq.offer(new int[]{0, 0, src});

        while (!pq.isEmpty()) {
            int[] info = pq.poll();
            int cost = info[0], k = info[1], place = info[2];
            if (k > K+1 || cost > best.getOrDefault(k * 1000 + place, Integer.MAX_VALUE))
                continue;
            if (place == dst)
                return cost;

            for (int nei = 0; nei < n; ++nei) if (graph[place][nei] > 0) {
                int newcost = cost + graph[place][nei];
                if (newcost < best.getOrDefault((k+1) * 1000 + nei, Integer.MAX_VALUE)) {
                    pq.offer(new int[]{newcost, k+1, nei});
                    best.put((k+1) * 1000 + nei, newcost);
                }
            }
        }

        return -1;
    }
}

python

class Solution(object):
    def findCheapestPrice(self, n, flights, src, dst, K):
        graph = collections.defaultdict(dict)
        for u, v, w in flights:
            graph[u][v] = w

        best = {}
        pq = [(0, 0, src)]
        while pq:
            cost, k, place = heapq.heappop(pq)
            if k > K+1 or cost > best.get((k, place), float('inf')): continue
            if place == dst: return cost

            for nei, wt in graph[place].iteritems():
                newcost = cost + wt
                if newcost < best.get((k+1, nei), float('inf')):
                    heapq.heappush(pq, (newcost, k+1, nei))
                    best[k+1, nei] = newcost

        return -1

复杂度分析

时间复杂度:O(E+nlogn),其中 EE 是航线的数量。

空间复杂度:O(n)O(n),优先级队列的大小。

记忆化搜索

class Solution {
    // 看到了DAG
    // dp[i][j][k],从i站到j站最多中转k站的最小代价!看题目描述自然想到这个状态
    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int K) {
        int[][] cost = new int[n][n];
        for(int i = 0; i < flights.length; i++){
            int st = flights[i][0];
            int en = flights[i][1];
            int price = flights[i][2];
            cost[st][en] = price;
        }
        int[][][] dp = new int[n][n][K+2];
        for(int i = 0; i < n; i++){
            for(int j = 0; j < n; j++){
                Arrays.fill(dp[i][j], -1);
            }
        }
        int res = f(cost, src, dst, K+1, dp);
        return res >= 1000000 ? -1 : res;
    }
    private int f(int[][] cost, int src, int dst, int K, int[][][] dp){
        if(K < 0){
            return 1000000;   
        }
        if(dp[src][dst][K] != -1){
            return dp[src][dst][K];
        }
        if(src == dst){
            return dp[src][dst][K] = 0;
        }
        int ans  = Integer.MAX_VALUE;
        for(int i = 0; i < cost.length; i++){
            if(cost[src][i] != 0){
                ans = Math.min(ans, cost[src][i] + f(cost, i, dst, K-1, dp));
            }
        }
        return dp[src][dst][K] = (ans == Integer.MAX_VALUE ? 1000000 : ans);
    }
}
上一篇:787. Cheapest Flights Within K Stops


下一篇:Python Pandas groupby并使用分组明智的计算来更改新列ala dplyr