【Leetcode】787. Cheapest Flights Within K Stops

题目地址:

https://leetcode.com/problems/cheapest-flights-within-k-stops/

给定一个非负权有向图,再给定一个源点 s s s,一个目标点 d d d,和一个整数 K K K,问从 s s s d d d中转站个数(其实就是路径去掉起点和终点后剩余的点数)不超过 K K K的最短路径长度。若不存在则返回 − 1 −1 1

法1:DFS + 剪枝。直接从起点开始DFS,当递归深度到达 K + 1 K+1 K+1(也就是路径边数)时就直接返回。也就是在暴搜所有能到达目标点且路径边数不超过 K + 1 K+1 K+1的路径的长度。同时用一个全局变量更新答案即可。代码如下:

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Solution {
    
    private int res = Integer.MAX_VALUE;
    
    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int K) {
        if (src == dst) {
            return 0;
        }
    
        Map<Integer, List<int[]>> graph = buildGraph(flights);
        boolean[] visited = new boolean[n];
        dfs(src, dst, 0, 0, K + 1, graph, visited);
        
        return res == Integer.MAX_VALUE ? -1 : res;
    }
    
    private void dfs(int cur, int dst, int curCost, int pathLen, int maxLen, Map<Integer, List<int[]>> graph, boolean[] visited) {
        if (cur == dst) {
            res = Math.min(res, curCost);
            return;
        }
        
        if (pathLen == maxLen) {
            return;
        }
        
        visited[cur] = true;
        if (graph.containsKey(cur)) {
            for (int[] next : graph.get(cur)) {
                int nextPoint = next[0], costFromCur = next[1];
                if (curCost + costFromCur >= res) {
                    continue;
                }
                
                dfs(nextPoint, dst, curCost + costFromCur, pathLen + 1, maxLen, graph, visited);
            }
        }
        visited[cur] = false;
    }
    
    private Map<Integer, List<int[]>> buildGraph(int[][] flights) {
        Map<Integer, List<int[]>> graph = new HashMap<>();
        for (int[] flight : flights) {
            graph.putIfAbsent(flight[0], new ArrayList<>());
            graph.get(flight[0]).add(new int[]{flight[1], flight[2]});
        }
        
        return graph;
    }
}

时空复杂度 O ( V + E ) O(V+E) O(V+E)

法2:Bellman-Ford算法。这个算法本质上是动态规划。设 f [ i ] [ v ] f[i][v] f[i][v]是从源点 s s s到顶点 v v v不超过 i i i条边的路径中最短那条路的长度。那么显然 f [ i ] [ v ] = min ⁡ ( u , v ) ∈ E { f [ i − 1 ] [ u ] + w ( u , v ) } f[i][v]=\min_{(u,v)\in E}\{f[i-1][u]+w(u,v)\} f[i][v]=(u,v)Emin{f[i1][u]+w(u,v)}如果目标点是 x x x的话,最终答案就是 f [ k ] [ x ] f[k][x] f[k][x]。为了节省空间,我们可以把 f f f简化为一维数组,但需要把 i − 1 i-1 i1的情形备份一份用来更新 i i i的情形。代码如下:

import java.util.Arrays;

public class Solution {
    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int K) {
        if (src == dst) {
            return 0;
        }
        
        // 用邻接矩阵建图
        int[][] graph = buildGraph(flights, n);
        // 就是描述中的f数组,记录从源点到该顶点的距离
        int[] dp = new int[n];
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[src] = 0;
        
        int[] backup = null;
        // 更新K + 1次
        for (int pass = 1; pass <= K + 1; pass++) {
        	// 备份一份
            backup = Arrays.copyOf(dp, n);
            // 遍历所有边,通过旧的dp[i](也就是backup[i])更新dp[j]
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                	// 无边,略过
                    if (graph[i][j] == Integer.MAX_VALUE) {
                        continue;
                    }
                    
                    // 这里为了防止溢出,需要判断一下正无穷的情况
                    if (backup[i] != Integer.MAX_VALUE) {
                        dp[j] = Math.min(dp[j], backup[i] + graph[i][j]);
                    }
                }
            }
        }
        
        return dp[dst] == Integer.MAX_VALUE ? -1 : dp[dst];
    }
    
    private int[][] buildGraph(int[][] flights, int n) {
        int[][] graph = new int[n][n];
        for (int[] row : graph) {
            Arrays.fill(row, Integer.MAX_VALUE);
        }
        
        for (int[] flight : flights) {
            graph[flight[0]][flight[1]] = flight[2];
        }
        
        return graph;
    }
}

时间复杂度 O ( K V 2 ) O(KV^2) O(KV2)

法3:BFS + 优先队列。其实就是模仿Dijkstra算法去做。但需要注意的是,这里BFS的时候,除非出队的顶点恰好是目标点(此时就直接返回答案了),否则是不能标记某个顶点已经算过的。也就是说,即使一个顶点算出过最短路,如果存在一个更长的路但是边数少,也需要加进优先队列里面,否则可能会漏解。代码如下:

import java.util.*;

public class Solution {
    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int K) {
        if (src == dst) {
            return 0;
        }
        
        // 用邻接矩阵建图
        int[][] graph = buildGraph(flights, n);
        
        // 这里int[]是三元组,第一个位置是花费,第二个是到达点,第三个是路径边数
        // 以从源点到当前点的花费做权重,开最小堆
        PriorityQueue<int[]> minHeap = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
        // 把源点加入堆
        minHeap.offer(new int[]{0, src, 0});
        
        while (!minHeap.isEmpty()) {
            int[] cur = minHeap.poll();
            int cost = cur[0], place = cur[1], pathLen = cur[2];
            // 如果取出来的点恰好是目标点,直接返回花费
            if (place == dst) {
                return cost;
            }
            
            // 如果当前点与源点的这条路径边数还不足K + 1,则对其邻接点拓展
            if (pathLen < K + 1) {
            	// 遍历其邻边
                for (int next = 0; next < n; next++) {
                	// 如果连通,就更新,并且加入堆
                    if (graph[place][next] != Integer.MAX_VALUE) {
                        minHeap.offer(new int[]{cost + graph[place][next], next, pathLen + 1});
                    }
                }
            }
        }
        
        return -1;
    }
    
    private int[][] buildGraph(int[][] flights, int n) {
        int[][] graph = new int[n][n];
        for (int[] row : graph) {
            Arrays.fill(row, Integer.MAX_VALUE);
        }
        
        for (int[] flight : flights) {
            graph[flight[0]][flight[1]] = flight[2];
        }
        
        return graph;
    }
}

时间复杂度 O ( E log ⁡ E ) O(E\log E) O(ElogE),空间 O ( V 2 ) O(V^2) O(V2)

注解:法3与通常的Dijkstra算法有若干不同之处:
第一,不需要开dist数组存储每个点与源点的最短路长度,也不需要开visited数组标记某个点最短路是否被算过。回顾一下传统的Dijkstra算法,visited数组的作用在于,当顶点出堆的时候,标记其最短路已经算出过,以后更新到该顶点的时候就要忽略这个顶点。但是这道题中不能这样判断,原因是,即使一个顶点 v v v出堆,我们也不能断定要求的不超过 k k k条边的最短路路径一定包含 v v v对应的那个最短路 P P P,有可能选的是路程更长但是边数更短的路,而不是 P P P。所以不能对 v v v做标记。类似的道理,在传统的Dijkstra算法中,dist数组存的是当前到某个点的最短路,但是这些信息事实上没什么用,即使发现了最短路也不能对其更新,否则就会产生上面说的“路程更长但是边数更短的路”的问题,从而漏解。

第二,一个顶点第一次出堆的时候,只有它恰好是目标点的时候,对应的最短路是正确的,别的点出堆的时候附带的最短路不一定是目标点最短路的子路径(也就是说,没有最优子结构的性质),原因就在于边数的限制。所以法 3 3 3本质上还是BFS,只是形似Dijkstra而已。

但是,我们可以证明:当一个顶点出堆的时候,它对应的花费就是从源点到该点的不超 k k k条边的最短路的花费,并且负责更新这条最短路的每个顶点的三元组都已经出堆。第二条是显然的,最短路路径上的点当然要出堆才能更新到当前出堆的点。注意到这个算法其实是保持路径长度不超过 k k k的并且 h ( v ) = 0 h(v)=0 h(v)=0的A*算法,而A*算法是能保证第一次出堆就得到最短路的,所以结论正确。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值