【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)。
相关推荐
hansang_IR8 小时前
【讲解】CSP-S2026 第一轮初赛
c++·算法
hanlin038 小时前
刷题笔记:力扣第76题-最小覆盖子串
笔记·算法·leetcode
Zane19948 小时前
比较排序跑不出O(nlogn)是数学上证明过的,计数、桶、基数排序凭什么能绕开这堵墙
算法·排序算法
tiger8658 小时前
大语言模型面试和题解,梳理中国主流开源 LLM 系列的发展脉络、技术路线与工程取舍
人工智能·gpt·深度学习·算法·自然语言处理·面试·transformer
我不会起名字3228 小时前
一天一道力扣Hot100(36):深度优先算法---组合总和
数据结构·c++·后端·python·算法·go
虚无的纽扣8 小时前
【力扣刷题】第三天:有效三角形的个数、快乐数
算法·排序算法
tiger8659 小时前
大语言模型面试和题解,Context Engineering 怎么做?长 Prompt、动态上下文与 Memory 管理
人工智能·深度学习·算法·语言模型·自然语言处理·面试·nlp
不会就选b9 小时前
算法日常・每日刷题--<贪心>17
算法
yuanxi2009 小时前
上药集团药品销售公司签署沪明医疗数链协议:PSS⁵视角下的跨区域数据合作机会
职场和发展·创业创新·学习方法
随便发挥11 小时前
Leetcode 剑指 Offer II 186. 文物朝代判断
leetcode