Files
leetcode-master/problems/kamacoder/0047.参会dijkstra堆.md
programmercarl 2c32229383 Update
2024-07-15 16:49:16 +08:00

930 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<p align="center"><strong><a href="./qita/join.md">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!</strong></p>
# dijkstra堆优化版精讲
[卡码网47. 参加科学大会](https://kamacoder.com/problempage.php?pid=1047)
【题目描述】
小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。
小明的起点是第一个车站,终点是最后一个车站。然而,途中的各个车站之间的道路状况、交通拥堵程度以及可能的自然因素(如天气变化)等不同,这些因素都会影响每条路径的通行时间。
小明希望能选择一条花费时间最少的路线,以确保他能够尽快到达目的地。
【输入描述】
第一行包含两个正整数,第一个正整数 N 表示一共有 N 个公共汽车站,第二个正整数 M 表示有 M 条公路。
接下来为 M 行每行包括三个整数S、E 和 V代表了从 S 车站可以单向直达 E 车站,并且需要花费 V 单位的时间。
【输出描述】
输出一个整数,代表小明从起点到终点所花费的最小时间。
输入示例
```
7 9
1 2 1
1 3 4
2 3 2
2 4 5
3 4 2
4 5 3
2 6 4
5 7 4
6 7 9
```
输出示例12
【提示信息】
能够到达的情况:
如下图所示,起始车站为 1 号车站,终点车站为 7 号车站,绿色路线为最短的路线,路线总长度为 12则输出 12。
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240227101345.png)
不能到达的情况:
如下图所示,当从起始车站不能到达终点车站时,则输出 -1。
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240227101401.png)
数据范围:
1 <= N <= 500;
1 <= M <= 5000;
## 思路
> 本篇我们来讲解 堆优化版dijkstra看本篇之前一定要先看 我讲解的 朴素版dijkstra否则本篇会有部分内容看不懂。
在上一篇中我们讲解了朴素版的dijkstra该解法的时间复杂度为 O(n^2),可以看出时间复杂度 只和 n (节点数量)有关系。
如果n很大的话我们可以换一个角度来优先性能。
在 讲解 最小生成树的时候,我们 讲了两个算法,[prim算法](./0053.寻宝-prim.md)(从点的角度来求最小生成树)、[Kruskal算法](./0053.寻宝-Kruskal.md)(从边的角度来求最小生成树)
这么在n 很大的时候,也有另一个思考维度,即:从边的数量出发。
当 n 很大,边 的数量 也很多的时候(稠密图),那么 上述解法没问题。
但 n 很大,边 的数量 很小的时候(稀疏图),是不是可以换成从边的角度来求最短路呢?
毕竟边的数量少。
有的录友可能会想n (节点数量)很大,边不就多吗? 怎么会边的数量少呢?
别忘了,谁也没有规定 节点之间一定要有边连接着,例如有一万个节点,只有一条边,这也是一张图。
了解背景之后,再来看 解法思路。
### 图的存储
首先是 图的存储。
关于图的存储 主流有两种方式: 邻接矩阵和邻接表
#### 邻接矩阵
邻接矩阵 使用 二维数组来表示图结构。 邻接矩阵是从节点的角度来表示图,有多少节点就申请多大的二维数组。
例如: grid[2][5] = 6表示 节点 2 链接 节点5 为有向图节点2 指向 节点5边的权值为6 套在题意里可能是距离为6 或者 消耗为6 等等)
如果想表示无向图grid[2][5] = 6grid[5][2] = 6表示节点2 与 节点5 相互连通权值为6。
如图:
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240222110025.png)
在一个 n 节点数为8 的图中,就需要申请 8 * 8 这么大的空间有一条双向边grid[2][5] = 6grid[5][2] = 6
这种表达方式(邻接矩阵) 在 边少,节点多的情况下,会导致申请过大的二维数组,造成空间浪费。
而且在寻找节点链接情况的时候,需要遍历整个矩阵,即 n * n 的时间复杂度,同样造成时间浪费。
邻接矩阵的优点:
* 表达方式简单,易于理解
* 检查任意两个顶点间是否存在边的操作非常快
* 适合稠密图,在边数接近顶点数平方的图中,邻接矩阵是一种空间效率较高的表示方法。
缺点:
* 遇到稀疏图,会导致申请过大的二维数组造成空间浪费 且遍历 边 的时候需要遍历整个n * n矩阵造成时间浪费
#### 邻接表
邻接表 使用 数组 + 链表的方式来表示。 邻接表是从边的数量来表示图,有多少边 才会申请对应大小的链表。
邻接表的构造如图:
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240223103713.png)
这里表达的图是:
* 节点1 指向 节点3 和 节点5
* 节点2 指向 节点4、节点3、节点5
* 节点3 指向 节点4节点4指向节点1。
有多少边 邻接表才会申请多少个对应的链表节点。
从图中可以直观看出 使用 数组 + 链表 来表达 边的链接情况 。
邻接表的优点:
* 对于稀疏图的存储,只需要存储边,空间利用率高
* 遍历节点链接情况相对容易
缺点:
* 检查任意两个节点间是否存在边,效率相对低,需要 O(V)时间V表示某节点链接其他节点的数量。
* 实现相对复杂,不易理解
#### 本题图的存储
接下来我们继续按照稀疏图的角度来分析本题。
在第一个版本的实现思路中,我们提到了三部曲:
1. 第一步,选源点到哪个节点近且该节点未被访问过
2. 第二步,该最近节点被标记访问过
3. 第三步更新非访问节点到源点的距离即更新minDist数组
在第一个版本的代码中,这三部曲是套在一个 for 循环里,为什么?
因为我们是从节点的角度来解决问题。
三部曲中第一步选源点到哪个节点近且该节点未被访问过这个操作本身需要for循环遍历 minDist 来寻找最近的节点。
同时我们需要 遍历所有 未访问过的节点,所以 我们从 节点角度出发代码会有两层for循环代码是这样的 注意代码中的注释标记两层for循环的用处
```CPP
for (int i = 1; i <= n; i++) { // 遍历所有节点第一层for循环
int minVal = INT_MAX;
int cur = 1;
// 1、选距离源点最近且未访问过的节点 第二层for循环
for (int v = 1; v <= n; ++v) {
if (!visited[v] && minDist[v] < minVal) {
minVal = minDist[v];
cur = v;
}
}
visited[cur] = true; // 2、标记该节点已被访问
// 3、第三步更新非访问节点到源点的距离即更新minDist数组
for (int v = 1; v <= n; v++) {
if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) {
minDist[v] = minDist[cur] + grid[cur][v];
}
}
}
```
那么当从 边 的角度出发, 在处理 三部曲里的第一步(选源点到哪个节点近且该节点未被访问过)的时候 ,我们可以不用去遍历所有节点了。
而且 直接把 边(带权值)加入到 小顶堆(利用堆来自动排序),那么每次我们从 堆顶里 取出 边 自然就是 距离源点最近的节点所在的边。
这样我们就不需要两层for循环来寻找最近的节点了。
了解了大体思路,我们再来看代码实现。
首先是 如何使用 邻接表来表述图结构,这是摆在很多录友面前的第一个难题。
邻接表用 数组+链表 来表示代码如下C++中 vector 为数组list 为链表, 定义了 n+1 这么大的数组空间)
```CPP
vector<list<int>> grid(n + 1);
```
不少录友,不知道 如何定义的数据结构,怎么表示邻接表的,我来给大家画一个图:
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240223103713.png)
图中邻接表表示:
* 节点1 指向 节点3 和 节点5
* 节点2 指向 节点4、节点3、节点5
* 节点3 指向 节点4
* 节点4 指向 节点1
大家发现图中的边没有权值,而本题中 我们的边是有权值的,权值怎么表示?在哪里表示?
所以 在`vector<list<int>> grid(n + 1);` 中 就不能使用int了而是需要一个键值对 来存两个数字,一个数表示节点,一个数表示 指向该节点的这条边的权值。
那么 代码可以改成这样: pair 为键值对可以存放两个int
```CPP
vector<list<pair<int,int>>> grid(n + 1);
```
举例来给大家展示 该代码表达的数据 如下:
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240223103904.png)
* 节点1 指向 节点3 权值为 1
* 节点1 指向 节点5 权值为 2
* 节点2 指向 节点4 权值为 7
* 节点2 指向 节点3 权值为 6
* 节点2 指向 节点5 权值为 3
* 节点3 指向 节点4 权值为 3
* 节点5 指向 节点1 权值为 10
这样 我们就把图中权值表示出来了。
但是在代码中 使用 `pair<int, int>` 很容易让我们搞混了第一个int 表示什么第二个int表示什么导致代码可读性很差或者说别人看你的代码看不懂。
那么 可以 定一个类 来取代 `pair<int, int>`
类(或者说是结构体)定义如下:
```CPP
struct Edge {
int to; // 邻接顶点
int val; // 边的权重
Edge(int t, int w): to(t), val(w) {} // 构造函数
};
```
这个类里有两个成员变量,有对应的命名,这样不容易搞混 两个int的含义。
所以 本题中邻接表的定义如下:
```CPP
struct Edge {
int to; // 链接的节点
int val; // 边的权重
Edge(int t, int w): to(t), val(w) {} // 构造函数
};
vector<list<Edge>> grid(n + 1); // 邻接表
```
(我们在下面的讲解中会直接使用这个邻接表的代码表示方式)
### 堆优化细节
其实思路依然是 dijkstra 三部曲:
1. 第一步,选源点到哪个节点近且该节点未被访问过
2. 第二步,该最近节点被标记访问过
3. 第三步更新非访问节点到源点的距离即更新minDist数组
只不过之前是 通过遍历节点来遍历边通过两层for循环来寻找距离源点最近节点。 这次我们直接遍历边,且通过堆来对边进行排序,达到直接选择距离源点最近节点。
先来看一下针对这三部曲,如果用 堆来优化。
那么三部曲中的第一步(选源点到哪个节点近且该节点未被访问过),我们如何选?
我们要选择距离源点近的节点(即:该边的权值最小),所以 我们需要一个 小顶堆 来帮我们对边的权值排序,每次从小顶堆堆顶 取边就是权值最小的边。
C++定义小顶堆,可以用优先级队列实现,代码如下:
```CPP
// 小顶堆
class mycomparison {
public:
bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
return lhs.second > rhs.second;
}
};
// 优先队列中存放 pair<节点编号,源点到该节点的权值>
priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pq;
```
`pair<int, int>`中 第二个int 为什么要存 源点到该节点的权值,因为 这个小顶堆需要按照权值来排序)
有了小顶堆自动对边的权值排序,那我们只需要直接从 堆里取堆顶元素(小顶堆中,最小的权值在上面),就可以取到离源点最近的节点了 (未访问过的节点,不会加到堆里进行排序)
所以三部曲中的第一步,我们不用 for循环去遍历直接取堆顶元素
```CPP
// pair<节点编号,源点到该节点的权值>
pair<int, int> cur = pq.top(); pq.pop();
```
第二步(该最近节点被标记访问过) 这个就是将 节点做访问标记,和 朴素dijkstra 一样 ,代码如下:
```CPP
// 2. 第二步,该最近节点被标记访问过
visited[cur.first] = true;
```
`cur.first` 是指取 `pair<int, int>` 里的第一个int即节点编号
第三步(更新非访问节点到源点的距离),这里的思路 也是 和朴素dijkstra一样的。
但很多录友对这里是最懵的,主要是因为两点:
* 没有理解透彻 dijkstra 的思路
* 没有理解 邻接表的表达方式
我们来回顾一下 朴素dijkstra 在这一步的代码和思路如果没看过我讲解的朴素版dijkstra这里会看不懂
```CPP
// 3、第三步更新非访问节点到源点的距离即更新minDist数组
for (int v = 1; v <= n; v++) {
if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) {
minDist[v] = minDist[cur] + grid[cur][v];
}
}
```
其中 for循环是用来做什么的 是为了 找到 节点cur 链接指向了哪些节点,因为使用邻接矩阵的表达方式 所以把所有节点遍历一遍。
而在邻接表中,我们可以以相对高效的方式知道一个节点链接指向哪些节点。
再回顾一下邻接表的构造(数组 + 链表):
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240223103713.png)
假如 加入的cur 是节点 2 那么 grid[2] 表示的就是图中第二行链表。 grid数组的构造我们在 上面 「图的存储」中讲过)
所以在邻接表中,我们要获取 节点cur 链接指向哪些节点,就是遍历 grid[cur节点编号] 这个链表。
这个遍历方式C++代码如下:
```CPP
for (Edge edge : grid[cur.first])
```
(如果不知道 Edge 是什么,看上面「图的存储」中邻接表的讲解)
`cur.first` 就是cur节点编号 参考上面pair的定义 pair<节点编号,源点到该节点的权值>
接下来就是更新 非访问节点到源点的距离,代码实现和 朴素dijkstra 是一样的,代码如下:
```CPP
// 3. 第三步更新非访问节点到源点的距离即更新minDist数组
for (Edge edge : grid[cur.first]) { // 遍历 cur指向的节点cur指向的节点为 edge
// cur指向的节点edge.to这条边的权值为 edge.val
if (!visited[edge.to] && minDist[cur.first] + edge.val < minDist[edge.to]) { // 更新minDist
minDist[edge.to] = minDist[cur.first] + edge.val;
pq.push(pair<int, int>(edge.to, minDist[edge.to]));
}
}
```
但为什么思路一样有的录友能写出朴素dijkstra但堆优化这里的逻辑就是写不出来呢
**主要就是因为对邻接表的表达方式不熟悉**
以上代码中cur 链接指向的节点编号 为 edge.to 这条边的权值为 edge.val ,如果对这里模糊的就再回顾一下 Edge的定义
```CPP
struct Edge {
int to; // 邻接顶点
int val; // 边的权重
Edge(int t, int w): to(t), val(w) {} // 构造函数
};
```
确定该节点没有被访问过,`!visited[edge.to]` 目前 源点到cur.first的最短距离minDist + cur.first 到 edge.to 的距离 edge.val 是否 小于 minDist已经记录的 源点到 edge.to 的距离 minDist[edge.to]
如果是的话,就开始更新操作。
即:
```CPP
if (!visited[edge.to] && minDist[cur.first] + edge.val < minDist[edge.to]) { // 更新minDist
minDist[edge.to] = minDist[cur.first] + edge.val;
pq.push(pair<int, int>(edge.to, minDist[edge.to])); // 由于cur节点的加入而新链接的边加入到优先级队里中
}
```
同时由于cur节点的加入源点又有可以新链接到的边将这些边加入到优先级队里中。
以上代码思路 和 朴素版dijkstra 是一样一样的,主要区别是两点:
* 邻接表的表示方式不同
* 使用优先级队列(小顶堆)来对新链接的边排序
### 代码实现
堆优化dijkstra完整代码如下
```CPP
#include <iostream>
#include <vector>
#include <list>
#include <queue>
#include <climits>
using namespace std;
// 小顶堆
class mycomparison {
public:
bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
return lhs.second > rhs.second;
}
};
// 定义一个结构体来表示带权重的边
struct Edge {
int to; // 邻接顶点
int val; // 边的权重
Edge(int t, int w): to(t), val(w) {} // 构造函数
};
int main() {
int n, m, p1, p2, val;
cin >> n >> m;
vector<list<Edge>> grid(n + 1);
for(int i = 0; i < m; i++){
cin >> p1 >> p2 >> val;
// p1 指向 p2权值为 val
grid[p1].push_back(Edge(p2, val));
}
int start = 1; // 起点
int end = n; // 终点
// 存储从源点到每个节点的最短距离
std::vector<int> minDist(n + 1, INT_MAX);
// 记录顶点是否被访问过
std::vector<bool> visited(n + 1, false);
// 优先队列中存放 pair<节点,源点到该节点的权值>
priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pq;
// 初始化队列源点到源点的距离为0所以初始为0
pq.push(pair<int, int>(start, 0));
minDist[start] = 0; // 起始点到自身的距离为0
while (!pq.empty()) {
// 1. 第一步,选源点到哪个节点近且该节点未被访问过 (通过优先级队列来实现)
// <节点, 源点到该节点的距离>
pair<int, int> cur = pq.top(); pq.pop();
if (visited[cur.first]) continue;
// 2. 第二步,该最近节点被标记访问过
visited[cur.first] = true;
// 3. 第三步更新非访问节点到源点的距离即更新minDist数组
for (Edge edge : grid[cur.first]) { // 遍历 cur指向的节点cur指向的节点为 edge
// cur指向的节点edge.to这条边的权值为 edge.val
if (!visited[edge.to] && minDist[cur.first] + edge.val < minDist[edge.to]) { // 更新minDist
minDist[edge.to] = minDist[cur.first] + edge.val;
pq.push(pair<int, int>(edge.to, minDist[edge.to]));
}
}
}
if (minDist[end] == INT_MAX) cout << -1 << endl; // 不能到达终点
else cout << minDist[end] << endl; // 到达终点最短路径
}
```
* 时间复杂度O(ElogE) E 为边的数量
* 空间复杂度O(N + E) N 为节点的数量
堆优化的时间复杂度 只和边的数量有关 和节点数无关,在 优先级队列中 放的也是边。
以上代码中,`while (!pq.empty())` 里套了 `for (Edge edge : grid[cur.first])`
`for` 里 遍历的是 当前节点 cur 所连接边。
那 当前节点cur 所连接的边 也是不固定的, 这就让大家分不清,这时间复杂度究竟是多少?
其实 `for (Edge edge : grid[cur.first])` 里最终的数据走向 是 给队列里添加边。
那么跳出局部代码,整个队列 一定是 所有边添加了一次,同时也弹出了一次。
所以边添加一次时间复杂度是 O(E) `while (!pq.empty())` 里每次都要弹出一个边来进行操作,在优先级队列(小顶堆)中 弹出一个元素的时间复杂度是 O(logE) ,这是堆排序的时间复杂度。
(当然小顶堆里 是 添加元素的时候 排序,还是 取数元素的时候排序这个无所谓时间复杂度都是O(E),总之是一定要排序的,而小顶堆里也不会滞留元素,有多少元素添加 一定就有多少元素弹出)
所以 该算法整体时间复杂度为 OElogE)
网上的不少分析 会把 n 节点的数量算进来这个分析是有问题的举一个极端例子在n 为 10000且是有一条边的 图里,以上代码,大家感觉执行了多少次?
`while (!pq.empty())` 中的 pq 存的是边,其实只执行了一次。
所以该算法时间复杂度 和 节点没有关系。
至于空间复杂度,邻接表是 数组 + 链表 数组的空间 是 N 有E条边 就申请对应多少个链表节点,所以是 复杂度是 N + E
## 拓展
当然也有录友可能想 堆优化dijkstra 中 我为什么一定要用邻接表呢,我就用邻接矩阵 行不行
也行的。
但 正是因为稀疏图,所以我们使用堆优化的思路, 如果我们还用 邻接矩阵 去表达这个图的话,就是 **一个高效的算法 使用了低效的数据结构,那么 整体算法效率 依然是低的**。
如果还不清楚为什么要使用 邻接表,可以再看看上面 我在 「图的存储」标题下的讲解。
这里我也给出 邻接矩阵版本的堆优化dijkstra代码
```CPP
#include <iostream>
#include <vector>
#include <list>
#include <climits>
using namespace std;
// 小顶堆
class mycomparison {
public:
bool operator()(const pair<int, int>& lhs, const pair<int, int>& rhs) {
return lhs.second > rhs.second;
}
};
int main() {
int n, m, p1, p2, val;
cin >> n >> m;
vector<vector<int>> grid(n + 1, vector<int>(n + 1, INT_MAX));
for(int i = 0; i < m; i++){
cin >> p1 >> p2 >> val;
// p1 指向 p2权值为 val
grid[p1][p2] = val;
}
int start = 1; // 起点
int end = n; // 终点
// 存储从源点到每个节点的最短距离
std::vector<int> minDist(n + 1, INT_MAX);
// 记录顶点是否被访问过
std::vector<bool> visited(n + 1, false);
// 优先队列中存放 pair<节点,源点到该节点的距离>
priority_queue<pair<int, int>, vector<pair<int, int>>, mycomparison> pq;
// 初始化队列源点到源点的距离为0所以初始为0
pq.push(pair<int, int>(start, 0));
minDist[start] = 0; // 起始点到自身的距离为0
while (!pq.empty()) {
// <节点, 源点到该节点的距离>
// 1、选距离源点最近且未访问过的节点
pair<int, int> cur = pq.top(); pq.pop();
if (visited[cur.first]) continue;
visited[cur.first] = true; // 2、标记该节点已被访问
// 3、第三步更新非访问节点到源点的距离即更新minDist数组
for (int j = 1; j <= n; j++) {
if (!visited[j] && grid[cur.first][j] != INT_MAX && (minDist[cur.first] + grid[cur.first][j] < minDist[j])) {
minDist[j] = minDist[cur.first] + grid[cur.first][j];
pq.push(pair<int, int>(j, minDist[j]));
}
}
}
if (minDist[end] == INT_MAX) cout << -1 << endl; // 不能到达终点
else cout << minDist[end] << endl; // 到达终点最短路径
}
```
* 时间复杂度O(E * (N + logE)) E为边的数量N为节点数量
* 空间复杂度O(log(N^2))
`while (!pq.empty())` 时间复杂度为 E while 里面 每次取元素 时间复杂度 为 logE和 一个for循环 时间复杂度 为 N 。
所以整体是 E * (N + logE)
## 总结
在学习一种优化思路的时候,首先就要知道为什么要优化,遇到了什么问题。
正如我在开篇就给大家交代清楚 堆优化方式的背景。
堆优化的整体思路和 朴素版是大体一样的,区别是 堆优化从边的角度出发且利用堆来排序。
很多录友别说写堆优化 就是看 堆优化的代码也看的很懵。
主要是因为两点:
* 不熟悉邻接表的表达方式
* 对dijkstra的实现思路还是不熟
这是我为什么 本篇花了大力气来讲解 图的存储,就是为了让大家彻底理解邻接表以及邻接表的代码写法。
至于 dijkstra的实现思路 ,朴素版 和 堆优化版本 都是 按照 dijkstra 三部曲来的。
理解了三部曲dijkstra 的思路就是清晰的。
针对邻接表版本代码 我做了详细的 时间复杂度分析,也让录友们清楚,相对于 朴素版,时间都优化到哪了。
最后 我也给出了 邻接矩阵的版本代码,分析了这一版本的必要性以及时间复杂度。
至此通过 两篇dijkstra的文章终于把 dijkstra 讲完了,如果大家对我讲解里所涉及的内容都吃透的话,详细对 dijkstra 算法也就理解到位了。
## 其他语言版本
### Java
```Java
import java.util.*;
class Edge {
int to; // 邻接顶点
int val; // 边的权重
Edge(int to, int val) {
this.to = to;
this.val = val;
}
}
class MyComparison implements Comparator<Pair<Integer, Integer>> {
@Override
public int compare(Pair<Integer, Integer> lhs, Pair<Integer, Integer> rhs) {
return Integer.compare(lhs.second, rhs.second);
}
}
class Pair<U, V> {
public final U first;
public final V second;
public Pair(U first, V second) {
this.first = first;
this.second = second;
}
}
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int m = scanner.nextInt();
List<List<Edge>> grid = new ArrayList<>(n + 1);
for (int i = 0; i <= n; i++) {
grid.add(new ArrayList<>());
}
for (int i = 0; i < m; i++) {
int p1 = scanner.nextInt();
int p2 = scanner.nextInt();
int val = scanner.nextInt();
grid.get(p1).add(new Edge(p2, val));
}
int start = 1; // 起点
int end = n; // 终点
// 存储从源点到每个节点的最短距离
int[] minDist = new int[n + 1];
Arrays.fill(minDist, Integer.MAX_VALUE);
// 记录顶点是否被访问过
boolean[] visited = new boolean[n + 1];
// 优先队列中存放 Pair<节点,源点到该节点的权值>
PriorityQueue<Pair<Integer, Integer>> pq = new PriorityQueue<>(new MyComparison());
// 初始化队列源点到源点的距离为0所以初始为0
pq.add(new Pair<>(start, 0));
minDist[start] = 0; // 起始点到自身的距离为0
while (!pq.isEmpty()) {
// 1. 第一步,选源点到哪个节点近且该节点未被访问过(通过优先级队列来实现)
// <节点, 源点到该节点的距离>
Pair<Integer, Integer> cur = pq.poll();
if (visited[cur.first]) continue;
// 2. 第二步,该最近节点被标记访问过
visited[cur.first] = true;
// 3. 第三步更新非访问节点到源点的距离即更新minDist数组
for (Edge edge : grid.get(cur.first)) { // 遍历 cur指向的节点cur指向的节点为 edge
// cur指向的节点edge.to这条边的权值为 edge.val
if (!visited[edge.to] && minDist[cur.first] + edge.val < minDist[edge.to]) { // 更新minDist
minDist[edge.to] = minDist[cur.first] + edge.val;
pq.add(new Pair<>(edge.to, minDist[edge.to]));
}
}
}
if (minDist[end] == Integer.MAX_VALUE) {
System.out.println(-1); // 不能到达终点
} else {
System.out.println(minDist[end]); // 到达终点最短路径
}
}
}
```
### Python
```python
import heapq
class Edge:
def __init__(self, to, val):
self.to = to
self.val = val
def dijkstra(n, m, edges, start, end):
grid = [[] for _ in range(n + 1)]
for p1, p2, val in edges:
grid[p1].append(Edge(p2, val))
minDist = [float('inf')] * (n + 1)
visited = [False] * (n + 1)
pq = []
heapq.heappush(pq, (0, start))
minDist[start] = 0
while pq:
cur_dist, cur_node = heapq.heappop(pq)
if visited[cur_node]:
continue
visited[cur_node] = True
for edge in grid[cur_node]:
if not visited[edge.to] and cur_dist + edge.val < minDist[edge.to]:
minDist[edge.to] = cur_dist + edge.val
heapq.heappush(pq, (minDist[edge.to], edge.to))
return -1 if minDist[end] == float('inf') else minDist[end]
# 输入
n, m = map(int, input().split())
edges = [tuple(map(int, input().split())) for _ in range(m)]
start = 1 # 起点
end = n # 终点
# 运行算法并输出结果
result = dijkstra(n, m, edges, start, end)
print(result)
```
### Go
```go
package main
import (
"container/heap"
"fmt"
"math"
)
// Edge 表示带权重的边
type Edge struct {
to, val int
}
// PriorityQueue 实现一个小顶堆
type Item struct {
node, dist int
}
type PriorityQueue []*Item
func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool {
return pq[i].dist < pq[j].dist
}
func (pq PriorityQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
}
func (pq *PriorityQueue) Push(x interface{}) {
*pq = append(*pq, x.(*Item))
}
func (pq *PriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
item := old[n-1]
*pq = old[0 : n-1]
return item
}
func dijkstra(n, m int, edges [][]int, start, end int) int {
grid := make([][]Edge, n+1)
for _, edge := range edges {
p1, p2, val := edge[0], edge[1], edge[2]
grid[p1] = append(grid[p1], Edge{to: p2, val: val})
}
minDist := make([]int, n+1)
for i := range minDist {
minDist[i] = math.MaxInt64
}
visited := make([]bool, n+1)
pq := &PriorityQueue{}
heap.Init(pq)
heap.Push(pq, &Item{node: start, dist: 0})
minDist[start] = 0
for pq.Len() > 0 {
cur := heap.Pop(pq).(*Item)
if visited[cur.node] {
continue
}
visited[cur.node] = true
for _, edge := range grid[cur.node] {
if !visited[edge.to] && minDist[cur.node]+edge.val < minDist[edge.to] {
minDist[edge.to] = minDist[cur.node] + edge.val
heap.Push(pq, &Item{node: edge.to, dist: minDist[edge.to]})
}
}
}
if minDist[end] == math.MaxInt64 {
return -1
}
return minDist[end]
}
func main() {
var n, m int
fmt.Scan(&n, &m)
edges := make([][]int, m)
for i := 0; i < m; i++ {
var p1, p2, val int
fmt.Scan(&p1, &p2, &val)
edges[i] = []int{p1, p2, val}
}
start := 1 // 起点
end := n // 终点
result := dijkstra(n, m, edges, start, end)
fmt.Println(result)
}
```
### Rust
### Javascript
### TypeScript
### PhP
### Swift
### Scala
### C#
### Dart
### C