leetcode
leetcode 1851 ~ 1900
前往目标城市的最小费用

前往目标城市的最小费用

难度:

标签:

题目描述

代码结果

运行时间: 260 ms, 内存: 29.9 MB


/*
题目思路:
同样的问题,但这次使用Java Stream API来简化代码。
我们将Dijkstra算法中的一些操作替换为Stream操作。
*/
import java.util.*;
import java.util.stream.*;

class Solution {
    public int minCost(int n, int[][] edges) {
        // 使用优先队列和流操作来处理每个城市的最小费用
        PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[1]));
        pq.offer(new int[]{0, 0});
        
        // 初始化每个城市的最小费用
        int[] minCost = IntStream.range(0, n).map(i -> Integer.MAX_VALUE).toArray();
        minCost[0] = 0;
        
        // 创建邻接表
        List<List<int[]>> graph = Stream.generate(ArrayList<int[]>::new).limit(n).collect(Collectors.toList());
        Arrays.stream(edges).forEach(edge -> graph.get(edge[0]).add(new int[]{edge[1], edge[2]}));
        
        // Dijkstra算法
        while (!pq.isEmpty()) {
            int[] cur = pq.poll();
            int city = cur[0];
            int cost = cur[1];
            
            if (city == n - 1) return cost;
            if (cost > minCost[city]) continue;
            
            graph.get(city).forEach(next -> {
                int nextCity = next[0];
                int nextCost = next[1] + cost;
                if (nextCost < minCost[nextCity]) {
                    minCost[nextCity] = nextCost;
                    pq.offer(new int[]{nextCity, nextCost});
                }
            });
        }
        return -1; // 如果找不到到达目标城市的路径
    }
}

解释

方法:

该题解使用了Dijkstra算法结合动态规划来解决最小费用寻路问题。首先,建立图的邻接表。然后,使用一个优先队列(通过heapq实现)来进行最小距离的贪心选择。维护一个二维数组dist,其中dist[i][j]表示到达节点i使用了j次折扣的最小费用。从源点开始,每次从优先队列中选出当前费用最小的点进行扩展,考虑两种情况:不使用折扣和使用折扣(如果剩余折扣次数大于0)。如果通过当前路线得到的新费用小于已记录的费用,则更新费用,并将新状态加入队列。如果到达终点,则返回当前的费用。若队列为空仍未到达终点,则返回-1表示无法到达。

时间复杂度:

O((E+V) * log(V) * D)

空间复杂度:

O(E + V * D)

代码细节讲解

🦆
在Dijkstra算法结合动态规划的实现中,如何确保每个节点的最小费用都得到正确更新,特别是在使用折扣的情况下?
在这种实现中,我们使用一个二维数组`dist`来记录每个节点在不同折扣次数下的最小费用。对于每个节点`x`,每当从`x`扩展到邻接节点`y`时,我们会考虑两种情况:一是直接连接不使用折扣,二是使用一次折扣(如果折扣次数`k`大于0)。我们会检查通过当前路径到达`y`(无论是使用折扣还是不使用)得到的新费用是否比`dist[y][k]`(或`dist[y][k-1]`)更小。如果是,我们就更新相应的费用,并将这个新状态(包括新的费用、节点、父节点和剩余折扣次数)加入优先队列。这种方法确保了每个节点的费用在考虑折扣的情况下都能得到正确且最优的更新。
🦆
你是如何决定何时使用折扣以及何时不使用折扣的?此决策是否基于某种优化逻辑?
在每次扩展节点时,我们同时考虑使用折扣和不使用折扣两种情况,并比较哪种选择可以获得较低的费用。具体来说,对于每一个邻接节点`y`和边的权重`w`,我们不仅考虑`dis + w`(不使用折扣的费用),还考虑`dis + w // 2`(使用一次折扣的费用),前提是剩余折扣次数`k`大于0。我们会比较这两个值与当前记录在`dist[y][k]`和`dist[y][k-1]`中的值,然后选择更小的一个来更新。这种决策基于贪心算法的优化逻辑,旨在每一步都选择当前可能的最小费用。
🦆
为什么在更新节点费用时,需要考虑是否来自同一个父节点(if y == fa continue)?这一步是否对算法的正确性或效率有重要影响?
这一设计主要是为了避免无意义的循环更新,即防止算法在相邻的两个节点之间来回更新,造成不必要的计算和可能的错误。当我们从节点`x`向节点`y`进行扩展时,如果`y`又回到了`x`的父节点`fa`,则意味着我们正在尝试重新进入已经处理过的路径,这并不会产生更优的结果,反而可能导致算法效率降低。因此,跳过这种情况可以减少冗余操作,提高算法效率,同时保持算法逻辑的清晰和正确性。

相关问题