【LeetCode 每日一题】3650. 边反转的最小路径总成本

Problem: 3650. 边反转的最小路径总成本

文章目录

  • [1. 整体思路](#1. 整体思路)
  • [2. 完整代码](#2. 完整代码)
  • [3. 时空复杂度](#3. 时空复杂度)
      • [时间复杂度: O ( M log ⁡ M ) O(M \log M) O(MlogM) 或 O ( M log ⁡ N ) O(M \log N) O(MlogN)](#时间复杂度: O ( M log ⁡ M ) O(M \log M) O(MlogM) 或 O ( M log ⁡ N ) O(M \log N) O(MlogN))
      • [空间复杂度: O ( N + M ) O(N + M) O(N+M)](#空间复杂度: O ( N + M ) O(N + M) O(N+M))

1. 整体思路

核心问题

在一个由 n 个节点组成的图中,求解从节点 0 到节点 n-1 的最小代价(最短路径)。

特殊的图构建逻辑

代码在构建图时处理 originalEdges 的方式比较特别:

  • 输入边 [u, v, cost]
  • 正向边 :从 uv,权重为 cost
  • 反向边 :从 vu,权重为 cost * 2
    这表明这是一张有向图,且这里的双向连接是不对称的(回去的代价是来的两倍)。

算法:Dijkstra 算法

这是一个标准的 Dijkstra 算法实现:

  1. 数据结构

    • 邻接表 (graph):存储图结构。
    • 距离数组 (minCosts):存储从起点到每个节点的当前已知最小代价。初始化为无穷大。
    • 优先队列 (minHeap):小顶堆,用于每次贪心地选取当前代价最小的节点进行扩展。
  2. 流程

    • 将起点 0 入堆,代价为 0。
    • 当堆不为空时:
      • 取出当前代价最小的节点 currentNode
      • 如果该节点就是终点 n-1,直接返回代价(因为 Dijkstra 保证了第一次取出的就是最短路径)。
      • 遍历该节点的所有邻居。如果经过当前节点到达邻居的代价比之前记录的更小,则更新 minCosts 并将邻居入堆。

2. 完整代码

java 复制代码
import java.util.*;

class Solution {
    public int minCost(int n, int[][] originalEdges) {
        // 1. 构建图 (邻接表)
        List<int[]>[] graph = new ArrayList[n];
        // 使用 lambda 表达式初始化每个节点的列表
        Arrays.setAll(graph, i -> new ArrayList<>());

        for (int[] edge : originalEdges) {
            int u = edge[0]; 
            int v = edge[1]; 
            int cost = edge[2]; 

            // 添加正向边 u -> v,权重为 cost
            graph[u].add(new int[]{v, cost});
            
            // 添加反向边 v -> u,权重为 cost * 2 (题目特定的逻辑)
            graph[v].add(new int[]{u, cost * 2});
        }

        // 2. 初始化 Dijkstra 算法所需的距离数组
        int[] minCosts = new int[n];
        // 初始化为最大值,表示尚未到达
        Arrays.fill(minCosts, Integer.MAX_VALUE);
        // 起点到自己的代价为 0
        minCosts[0] = 0; 

        // 3. 优先队列 (小顶堆),存储 int[]{cost, node}
        // 按 cost 从小到大排序
        PriorityQueue<int[]> minHeap = new PriorityQueue<>((a, b) -> a[0] - b[0]);
        // 将起点加入堆
        minHeap.offer(new int[]{0, 0}); 

        while (!minHeap.isEmpty()) {
            // 取出当前累计代价最小的节点
            int[] current = minHeap.poll();
            int currentCost = current[0];
            int currentNode = current[1];

            // 延迟删除/过时检查:
            // 如果从堆中取出的代价比 minCosts 记录的要大,说明该节点已经被更优的路径更新过并处理过了,
            // 这是一个"旧版本"的状态,直接跳过。
            if (currentCost > minCosts[currentNode]) {
                continue;
            }

            // 如果到达终点,直接返回当前代价
            // Dijkstra 保证第一次从堆中弹出终点时,路径是最短的
            if (currentNode == n - 1) {
                return currentCost;
            }

            // 遍历所有邻居进行松弛操作
            for (int[] neighborEntry : graph[currentNode]) {
                int nextNode = neighborEntry[0];
                int edgeCost = neighborEntry[1];
                int newTotalCost = currentCost + edgeCost;

                // 如果发现了更短的路径
                if (newTotalCost < minCosts[nextNode]) {
                    // 更新最小代价
                    minCosts[nextNode] = newTotalCost; 
                    // 将新状态加入堆
                    minHeap.offer(new int[]{newTotalCost, nextNode}); 
                }
            }
        }

        // 如果堆空了还没到达 n-1,说明无法到达,返回 -1
        return -1; 
    }
}

3. 时空复杂度

假设节点数为 N N N,原始边数组 originalEdges 的长度为 M M M。

时间复杂度: O ( M log ⁡ M ) O(M \log M) O(MlogM) 或 O ( M log ⁡ N ) O(M \log N) O(MlogN)

  • 构图 :遍历 M M M 条边,每条边添加两次(正向和反向),操作为 O ( 1 ) O(1) O(1)。总共 O ( M ) O(M) O(M)。实际边数 E = 2 M E = 2M E=2M。
  • Dijkstra 算法
    • 在最坏情况下(密集图),每个节点和每条边都会被处理。
    • 每个节点最多从堆中弹出一次。
    • 每条边最多会导致一次 offer 操作(松弛成功)。
    • 堆中最多可能有 E E E 个元素。
    • 总时间复杂度为 O ( E log ⁡ E ) O(E \log E) O(ElogE)。由于 E = 2 M E = 2M E=2M,且 log ⁡ ( 2 M ) ≈ log ⁡ M \log(2M) \approx \log M log(2M)≈logM,所以通常写为 O ( M log ⁡ M ) O(M \log M) O(MlogM)。
    • 或者也可以写为 O ( E log ⁡ N ) O(E \log N) O(ElogN),因为堆内有效元素其实对应节点数,但懒删除机制会导致堆大小接近 E E E。
  • 结论 : O ( M log ⁡ M ) O(M \log M) O(MlogM)。

空间复杂度: O ( N + M ) O(N + M) O(N+M)

  • 邻接表 :存储 N N N 个头节点和 2 M 2M 2M 条边,空间为 O ( N + M ) O(N + M) O(N+M)。
  • 距离数组 : O ( N ) O(N) O(N)。
  • 优先队列 :最坏情况下可能存储 O ( M ) O(M) O(M) 个元素。
  • 结论 : O ( N + M ) O(N + M) O(N+M)。
相关推荐
j_xxx404_2 小时前
C++算法入门:滑动窗口合集(长度最小的子数组|无重复字符的最长字串|)
开发语言·c++·算法
xhbaitxl2 小时前
算法学习day29-贪心算法
学习·算法·贪心算法
橘颂TA2 小时前
【剑斩OFFER】算法的暴力美学——力扣 1765 题:地图中的最高点
算法·leetcode·职场和发展·结构与算法
Full Stack Developme2 小时前
算法与数据结构,到底是怎么节省时间和空间的
数据结构·算法
棱镜Coding2 小时前
LeetCode-Hot100 28.两数相加
算法·leetcode·职场和发展
m0_561359672 小时前
C++中的过滤器模式
开发语言·c++·算法
AI科技星2 小时前
加速运动电荷产生引力场方程求导验证
服务器·人工智能·线性代数·算法·矩阵
啊阿狸不会拉杆2 小时前
《数字信号处理》第9章:序列的抽取与插值——多抽样率数字信号处理基础
算法·matlab·信号处理·数字信号处理·dsp
what丶k2 小时前
深入理解贪心算法:从原理到经典实践
算法·贪心算法·代理模式