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]。 - 正向边 :从
u到v,权重为cost。 - 反向边 :从
v到u,权重为cost * 2。
这表明这是一张有向图,且这里的双向连接是不对称的(回去的代价是来的两倍)。
算法:Dijkstra 算法
这是一个标准的 Dijkstra 算法实现:
-
数据结构:
- 邻接表 (
graph):存储图结构。 - 距离数组 (
minCosts):存储从起点到每个节点的当前已知最小代价。初始化为无穷大。 - 优先队列 (
minHeap):小顶堆,用于每次贪心地选取当前代价最小的节点进行扩展。
- 邻接表 (
-
流程:
- 将起点
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)。