fucking-algorithm icon indicating copy to clipboard operation
fucking-algorithm copied to clipboard

旅游省钱大法:加权最短路径 :: labuladong的算法小抄

Open utterances-bot opened this issue 2 years ago • 11 comments

文章链接点这里:旅游省钱大法:加权最短路径

评论礼仪 见这里,违者直接拉黑。

utterances-bot avatar Dec 02 '21 11:12 utterances-bot

这里的k的判断其实是有问题的吧?k==0只是代表没有了中转的机会,并不是不可达(还可直接从起点到终点),所以应该将if (k == 0)改为if (k < 0)吧?

LovesAsuna avatar Dec 02 '21 11:12 LovesAsuna

另外其实还有另外一种思路,即倒着过来求,将dp定义为dp(int start, int k),即当前中转次数为k时,从start到dst的最短路径,对应的dp要更改为

    // 在中转次数为k的情况下从start到dst的最短路径
    public int dp(int start, int k) {
        if (start == dst) {
            return 0;
        }
        if (k > this.k) {
            return -1;
        }
        if (memo[start][k] != -666) {
            return memo[start][k];
        }
        int res = Integer.MAX_VALUE;
        if (outdegree.containsKey(start)) {
            for (int[] element : outdegree.get(start)) {
                int to = element[0];
                int price = element[1];
                int subProblem = dp(to, k + 1);
                if (subProblem == -1) continue;
                res = Math.min(res, subProblem + price);
            }
        }
        memo[start][k] = res == Integer.MAX_VALUE ? -1 : res;
        return memo[start][k];
    }

此时也要注意对k的判断

LovesAsuna avatar Dec 02 '21 12:12 LovesAsuna

@LovesAsuna 嗯,k == 0 代表没有中转,当起点等于终点的时候确实是一种可行情况,但是:

1、这种边界问题的关键是看题目给的数据边界,题目说了 src != dst。所以你说的没问题,但单就这道题来说,k == 0 直接返回 -1 也是没问题的。

2、我们在一开始给输入的 k 做了 +1 的处理,题目给定的 k 的范围是大于等于 0,那么加一后 k 也不可能等于 0。

labuladong avatar Dec 16 '21 15:12 labuladong

@LovesAsuna 我上面说的是 dp 函数递归开始时的情况,我又仔细理解了一下你的问题,你问的应该是递归的过程中,s 被视作起点不断变化,如果当 k == 0s 恰好就是 src,这种情况不应该返回 -1。

是的,这就是我们找到答案的时候,实际上这种情况算法也没有返回 -1,而是返回了 0,因为判断 s == src 的 base case 在前,k == 0 的判断在后。

labuladong avatar Dec 16 '21 15:12 labuladong

呜呜呜看了这节的题目没看大佬分析,自己尝试自己分析一下,没想到真的写出来了,写的代码有些粗糙劣质~~现在就去完善,谢谢大佬

class Solution {
    int[][] memo;
    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
        memo = new int[n+1][k+2];
        return dp(n, flights, src, dst, k+1);
    }
    public int dp(int n, int[][] flights, int src, int dst, int k){
        // base case
        if(k < 0){
            return -1;
        }else if(src == dst){
            return 0;
        }

        if(memo[src][k] != 0){
           return memo[src][k];
        }
        int res = Integer.MAX_VALUE;
        for(int[] f : flights){
            if(f[0] != src)
                continue;
            int subProblem = dp(n, flights, f[1], dst, k-1);
            if(subProblem == -1){
                continue;
            }
            res = Math.min(res, subProblem + f[2]);
        }

        memo[src][k] = res==Integer.MAX_VALUE?-1:res;
        return memo[src][k];
    }
}

zzbb9944 avatar Feb 17 '22 13:02 zzbb9944

@zzbb9944 千里之行,始于足下,加油!

labuladong avatar Feb 22 '22 06:02 labuladong

这个自底向上的方法 ,比较难想清楚,因为这里的i,j 和之前的dp问题不太一样了,这里的i表示中转次数,比较抽象,且要注意 最后需要对最后一行的结果进行求最小值,不懂的,自己画个二维数组看看。 这个比较抽象,如果看不明白,还是看东哥的解答。东哥的解答很清楚,谢谢

class Solution {
    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
        
        int[][] dp = new int[k+1][n];// [i][j] 表示 从 开始节点 到 j节点 中转 i次的最少价格

        for(int i = 0; i<k+1;i++){
            Arrays.fill(dp[i],10000*101+1);//全部初始化成为 无法取到的值
            //这里为什么初始化成这个样子 需要看看题目里面的限制
            //但是如果初始化成 MAX_VALUE 是有bug的 会数值溢出  具体什么原因 自己想
        }

        for(int[] flight: flights){
            int from = flight[0];
            int to = flight[1];
            int cost = flight[2];
            if(src == from){
                dp[0][to] = cost;
                //中转0 次 从 开始节点 到 to这个节点的花费 
            }
        }
        //中转 i次
        for(int i = 1;i<k+1;i++){
            for(int[] flight : flights){
                int from = flight[0];
                int to = flight[1];
                int cost = flight[2];
                //从开始节点  中转 i 次 到达 to 需要的最少花费
                //在两种中做选择
                
                //从开始处 中转i-1次到达 from(to的前一个节点) 然后 再从前一个节点 到达目标节点位置
                //dp[i][to] 有多种方式到达 to ,这个里面 有最小值 也可能有最大值 反正直接比较就完事了
                dp[i][to] = Math.min(dp[i][to],dp[i-1][from]+cost);
            }
        }
        int result = 10000*101+1;
        //在所有可能中 找到到达 dst 位置的 最小的cost 作为结果
        for(int i = 0; i<k+1;i++){
            result = Math.min(result,dp[i][dst]);
        }
        return result == 10000*101+1 ? -1: result;

    }
}

zhongweiLeex avatar Mar 24 '22 03:03 zhongweiLeex

提供一个较粗糙的C++版本的实现,供参考。

class Solution {
public:
    int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int k) {
        //梳理数据结构
        vector<vector<int>> graphs(n,vector<int>());
        vector<vector<int>> weights(n,vector<int>(n,-1));
        for(int i=0;i<flights.size();++i){
            graphs[flights[i][1]].emplace_back(flights[i][0]);
            weights[flights[i][0]][flights[i][1]]=flights[i][2];
        }
        //动态规划算法
        vector<vector<int>> mem(n,vector<int>(k+1,-1));
        int res=dp(src,dst,k,graphs,weights,mem);
        return res!=1e9?res:-1;
    }

    int dp(int from,int to,int k,vector<vector<int>> & graphs,vector<vector<int>>& weights,vector<vector<int>> &mem){
        if(from==to) return 0; 
        if(k<0) return 1e9;
        if(mem[to][k]!=-1) return mem[to][k];

        int res=1e9;
        for(int i=0;i<graphs[to].size();++i){
            int candidate=graphs[to][i];
            res=std::min(res,dp(from,candidate,k-1,graphs,weights,mem)+weights[candidate][to]);
        }
        mem[to][k]=res;
        return mem[to][k];

    }
};

dreamyouonly avatar May 03 '22 06:05 dreamyouonly

Javascript BFS版本:

var findCheapestPrice = function(n, flights, src, dst, k) {
  // 构建邻接表
  const map = new Array(n).fill(0).map((_, i) => ({id: i, edges: []}))
  flights.forEach(flight => {
    const [from, to, price] = flight
    map[from].edges.push({to, price})
  })

  let res = new Array(n).fill(Number.MAX_SAFE_INTEGER)
  let depth = 0; // 记录深度中转站
  let q = [] // TODO: 换成优先级队列
  q.push([src, 0])

  // BFS
  while(q.length && depth <= k + 1) {
    let len = q.length
    while (len) {
      const [cur, curPrice] = q.shift()
      len--
      if (curPrice >= res[cur]) continue
      res[cur] = curPrice
      for(const edge of map[cur].edges) {
        q.push([edge.to, edge.price + curPrice])
      }
    }

    depth++

  }
  return res[dst] === Number.MAX_SAFE_INTEGER ? -1 : res[dst]
};

jrr997 avatar May 21 '22 09:05 jrr997

这个自底向上的方法 ,比较难想清楚,因为这里的i,j 和之前的dp问题不太一样了,这里的i表示中转次数,比较抽象,且要注意 最后需要对最后一行的结果进行求最小值,不懂的,自己画个二维数组看看。 这个比较抽象,如果看不明白,还是看东哥的解答。东哥的解答很清楚,谢谢

class Solution {
    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
        
        int[][] dp = new int[k+1][n];// [i][j] 表示 从 开始节点 到 j节点 中转 i次的最少价格

        for(int i = 0; i<k+1;i++){
            Arrays.fill(dp[i],10000*101+1);//全部初始化成为 无法取到的值
            //这里为什么初始化成这个样子 需要看看题目里面的限制
            //但是如果初始化成 MAX_VALUE 是有bug的 会数值溢出  具体什么原因 自己想
        }

        for(int[] flight: flights){
            int from = flight[0];
            int to = flight[1];
            int cost = flight[2];
            if(src == from){
                dp[0][to] = cost;
                //中转0 次 从 开始节点 到 to这个节点的花费 
            }
        }
        //中转 i次
        for(int i = 1;i<k+1;i++){
            for(int[] flight : flights){
                int from = flight[0];
                int to = flight[1];
                int cost = flight[2];
                //从开始节点  中转 i 次 到达 to 需要的最少花费
                //在两种中做选择
                
                //从开始处 中转i-1次到达 from(to的前一个节点) 然后 再从前一个节点 到达目标节点位置
                //dp[i][to] 有多种方式到达 to ,这个里面 有最小值 也可能有最大值 反正直接比较就完事了
                dp[i][to] = Math.min(dp[i][to],dp[i-1][from]+cost);
            }
        }
        int result = 10000*101+1;
        //在所有可能中 找到到达 dst 位置的 最小的cost 作为结果
        for(int i = 0; i<k+1;i++){
            result = Math.min(result,dp[i][dst]);
        }
        return result == 10000*101+1 ? -1: result;

    }
}

自己该自己的代码 , 之前的代码感觉写的不够精简, 重构一下

class Solution {
    public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
        // dp[i][j] 表示 经过 j个中转站到达 i 的最低费用
        //k+1+1 的原因是 将最后一个节点也理解成中转节点 
        int[][] dp = new int[n][k+2];
        //初始化 , 到达所有节点的初始化为 最大值 , 因为要求最低费用
        for(int i = 0 ; i< n ; i++){
            Arrays.fill(dp[i],Integer.MAX_VALUE);
        }
        for(int j = 0; j< k+2 ; j++){
            dp[src][j] = 0;//任何src 到src 最小都是 0 
        }
        
        for(int j = 1; j< k+2 ; j++){
            for(int[] flight : flights){
                /* 
                    flight[0] form
                    flight[1] to
                    flight[2] wight
                */
                //这一步防止后面的dp[flight[0]][j-1] + flight[2] 这一步出现 Integer.MAX_VALUE 参加运算 出现 数值溢出现象
                //也可以在初始化的时候 初始化成一个较大的数 不一定是Integer.MAX_VALUE
                if(dp[flight[0]][j-1] == Integer.MAX_VALUE){
                    continue;
                }
                dp[flight[1]][j] = Math.min(dp[flight[1]][j],dp[flight[0]][j-1] + flight[2]);
            }
        }
        return dp[dst][k+1] == Integer.MAX_VALUE ? -1 : dp[dst][k+1];

    }
}

zhongweiLeex avatar Jul 24 '22 04:07 zhongweiLeex

贴个C++解法,状态选的是K和起点而非K和终点(结果相同),用二维数组+pair类型直接构造有向加权图,常规DP

class Solution {
    vector<vector<int>> memo;
    vector<vector<pair<int, int>>> graph;
public:
    void buildGraph(vector<vector<int>>& flights, int n) {
        graph.assign(n, vector<pair<int, int>>());
        for (auto flight: flights) {
            int from = flight[0];
            int to = flight[1];
            int price = flight[2];
            graph[from].emplace_back(to, price);
        }
    }

    //  函数返回从src到dst的最多经过K次中转的最便宜价格
    int dp(vector<vector<pair<int, int>>>& graph, int src, int dst, int k) {
        if (src == dst) return 0;                //  base case
        if (k == -1) return INT_MAX;             //  达到中转上限
        if (memo[src][k] != -1) return memo[src][k];
        int res = INT_MAX;
        for (auto neighbor: graph[src]) {
            int node = neighbor.first;
            int price = neighbor.second;
            int subProb = dp(graph, node, dst, k - 1);
            if (subProb == INT_MAX) continue;
            res = min(res, subProb + price); 
        }
        return memo[src][k] = res;
    }

    int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int k) {
        buildGraph(flights, n);
        memo.assign(n, vector<int> (k + 1, -1));
        int res = dp(graph, src, dst, k);
        return res == INT_MAX ? -1 : res;
    }
};

lhcezx avatar Jul 28 '22 09:07 lhcezx

python

# 计算每个节点的入度以及对应价格
        indegree = {}
        for item in flights:
            start, to, price = item[0], item[1], item[2]
            if to not in indegree:
                indegree[to] = []
            indegree[to].append([start, price])
        k += 1 # k个stop = k+1条边,将中转站个数转化成边的条数
        memo = [[-1 for _ in range(k+1)] for __ in range(n)]
        
        # dp定义:从 src 出发,大K 步之内到达 end 的最短路径权重
        def dp(end, K):
            nonlocal src, indegree, memo
            # base case
            if end == src:
                return 0
            if K == 0:  # 没有剩余步数了
                return -1
            if memo[end][K] != -1:
                return memo[end][K]
            
            res = float('inf') # 初始化:K步内src到end到最短权重路径
            # 只有当end节点有入度时,才是可以到达点路径
            if end in indegree:
                for sub in indegree[end]:# 分解问题
                    start, price = sub[0], sub[1]
                    # 从src到达end前置节点所需的最短路径权重
                    subProblem = dp(start, K-1)
                    if subProblem != -1: # 跳过无解问题
                        res = min(res, subProblem+price)
                        
            memo[end][K] = res if res != float('inf') else -1
            return memo[end][K]
        return dp(dst, k)

Xingyue0310 avatar Sep 02 '22 20:09 Xingyue0310