目录
12月近一个月没更新了, 今天心血来潮更新一下吧。 笔者由于最近准备期末考试, 每天敲代码和写算法题的时间大幅缩水了。 再过几天就是数据结构,算法,计网的考试了。 临近期末周, 给12月份也补上一篇吧。 -2024/12/30
本篇是图论篇的最短路径算法, 最短路径算法有很多,Bellman-Foyid, SPFA, A*. 这里先写一篇经典的单源最短路径算法Dijkstra算法
。
前置知识和大致介绍
数据结构
和离散数学
都有讲
D
i
j
k
s
t
r
a
Dijkstra
Dijkstra算法, 读者应当熟悉一下路径,最短路径,连通性这些图论术语的基本概念, 解释这些术语非本篇的重点。
前置知识: 建图(本篇主要是基于链式前向星建图), 数据结构:堆
编程语言: C++/Java
使用: 可以借助ChatGPT-o1
转化成对应语言的代码。
本篇解决一个问题, 如何求
u
−
>
v
u->v
u−>v的最短路径。
借此引出单源最短路径问题。
在限定条件下, 可以采用
D
i
j
k
s
t
r
a
Dijkstra
Dijkstra算法求解单源最短路径。
三种实现方式:
- 朴素解法(暴力解法)—不说明, 时间复杂度太差。
- 最小堆(优先队列)实现: 稀疏图推荐。 这是本篇说明的
- 反向索引堆:稠密图推荐。(本篇不说明, 因为上还没有写反向索引堆这种变种堆数据结构)。
Dijkstra算法是经典的最短路径算法, 一种动态规划和贪心的策略应用(主要是贪心, 它因为具有最优子结构同样隶属动态规划应用范畴)。
主要内容
0. 历史及意义
Dijkstra算法的发明人及历史
Dijkstra算法(Dijkstra’s Algorithm)由荷兰计算机科学家埃茨赫尔·韦伯·戴克斯特拉(Edsger Wybe Dijkstra
,1930—2002)于1959年提出。
Dijkstra
是计算机科学的先驱人物之一,在程序设计、算法和软件工程等多个领域都有重要贡献。
名字是音译。 可参考wiki百科
-
算法提出历史
- Dijkstra算法发表年份:1959年,最初发表于一篇题为“解决图中最短路径问题的笔算方法”(A Note on Two Problems in Connexion with Graphs)的论文中。
- 当时的计算机科学处于早期发展阶段,图论算法尚未系统化。
Dijkstra
结合实际需求,提出了一种计算非负权重图上单源最短路径的高效方法,因其简单性和可操作性,迅速在学术和工程领域得到认可。
-
算法简介
- 算法具体介绍详解下文
Dijstra
算法实现。
- 算法具体介绍详解下文
-
影响与意义
- Dijkstra算法被广泛用于交通规划、网络路由、导航系统、通信网络等领域。
- 为后续图最短路径算法打基础(如
Bellman-Ford算法
、Floyd-Warshall算法
、A*
等)的发展。 - 计算机网络中路由协议。
- 最早的一批的图论算法, 现代图论算法的奠基成果之一。
1. 研究最短路径的对象是什么?
最短路径问题研究的对象是带权图,但需满足以下限制:
- 权值非负(即所有边的权值 ≥ 0 \geq 0 ≥0)。
注意:对于图的方向性和连通性均无要求。
2. 什么是最短路径?
本部分简单回顾图论知识, 进行最短路径定义。
路径:
-
给定一个路径 p p p,其形式为:
[ p = v 1 → v 2 → v 3 → ⋯ → v k ] [ p = v_1 \to v_2 \to v_3 \to \dots \to v_k ] [p=v1→v2→v3→⋯→vk]
这里的 p 是一条由顶点组成的线性序列,描述了从 v 1 v_1 v1 到 v k v_k vk 的连接方式。 -
每条路径的权重 w ( p ) w(p) w(p) 是其所有边的权值之和:
[ w ( p ) = ∑ i = 1 k − 1 w ( v i , v i + 1 ) ] [ w(p) = \sum_{i=1}^{k-1} w(v_i, v_{i+1}) ] [w(p)=∑i=1k−1w(vi,vi+1)] -
最短路径即为所有从起点到终点的路径中,总权值 w ( p ) w(p) w(p)最小的那条路径。
通俗定义:最短路径就是从起点 (A) 到终点 (B) 的距离最近的路径。
引入一种数学描述
δ
(
u
,
v
)
:
u
−
>
v
的最短路径
\delta(u,v): u->v的最短路径
δ(u,v):u−>v的最短路径。
它存在三种可能的结果
δ
(
u
,
v
)
=
−
∞
\delta(u,v)=-\infty
δ(u,v)=−∞,
δ
(
u
,
v
)
=
∞
\delta(u,v)=\infty
δ(u,v)=∞,
δ
(
u
,
v
)
>
=
0
,
且是一个确定的数值
.
\delta(u,v)>=0,且是一个确定的数值.
δ(u,v)>=0,且是一个确定的数值.
如下我们将讨论为什么出现这三种情况。
3. 为什么权值不能为负数?
权值非负是Dijkstra算法的一项核心要求,原因如下:
-
负权边可能导致最短路径不存在
- 负权边是一条无向边亦或者存在一对相同顶点的负权值有向边。 如, w ( u , v ) = − 5 w(u,v)=-5 w(u,v)=−5, 那么所有能到达 u , v u,v u,v的所有顶点均可反复绕这条边降低自己的权值, 直到达到 − ∞ -\infty −∞.
- 上面的一条推广就是当前情况。 若图中存在负权值环,路径上的总权值可以通过反复绕环降低至任意负值,直到达到 − ∞ -\infty −∞.。
示例:
若路径环 ( C = v i → v j → v k → v i ) (C = v_i \to v_j \to v_k \to v_i) (C=vi→vj→vk→vi),其总权重 ( w ( C ) < 0 ) (w(C) < 0) (w(C)<0),那么对于所有能够通往环 ( C ) (C) (C) 的顶点,都无法找到一个有限的最短路径。- 属于最短路径为 δ ( u , v ) = − ∞ \delta(u,v)=-\infty δ(u,v)=−∞的情况。
-
权值非负使贪心策略成立
- Dijkstra算法通过逐步扩展最短路径集合的方式寻找解,假设“当前最短路径一定是最终解的一部分”。这种贪心只能在权值非负时才能保证正确性。
-
为什么负权边“可能”导致最短路径不存在
?- 如果图中仅存在单条分散的负权有向边,最短路径依然可能存在。例如:
( A → B ) (A \to B) (A→B) 权重为 -5,但 (B) 无其他负权边,仍能找到最短路径。
最短路径为 δ ( u , v ) = − ∞ \delta(u,v)=-\infty δ(u,v)=−∞的情况。
- 如果图中仅存在单条分散的负权有向边,最短路径依然可能存在。例如:
-
最短路径不存在的另一种可能
- 如果图中两个顶点 u , v u,v u,v,它们之间本身不可达的。 换言之, 它们之间甚至不存在路径。 那么自然无从谈起最短路径。
- 如果出现 δ ( u , v ) = + ∞ \delta(u,v)=+\infty δ(u,v)=+∞, 那么意味着u,v不可达。 反过来也是如此。
- 属于第三种情况。
-
为什么要求图权值非负而不要求图一定连通呢?
- 因为权值为负数, 会引起贪心策略失效, 不停地更新最短路径, 还会干扰其它的可达顶点最短距离的求解。
- 不要求连通性的原因:
Dijkstra
算法只关心同一个连通分量源点到顶点之间的距离。其它连通分量之间不可达的关系可以明确用 + ∞ +\infty +∞区分。这对于求解本身连通分量点点之间最短路径无影响。
4. 什么是Dijkstra算法
和单源最短路径
d i j k s t r a dijkstra dijkstra算法是一种单源最短路径算法。
单源最短路径:给定一个源节点, 可以求出源节点到与源节点同一连通分量的其它节点之间的最短距离, 对于不可达顶点认为它们的距离为 + ∞ +\infty +∞, 实际代码实现通常为系统最大值。
朴素算法时间复杂度:
O
(
m
2
)
O(m^2)
O(m2), 暴力。
普通堆优化的时间复杂度:
O
(
m
l
o
g
m
)
O(mlogm)
O(mlogm),这种算法时间复杂度已经比较好了,适合稀疏图(边数<顶点数)
反向索引堆的时间复杂度:
O
(
m
l
o
g
n
)
O(mlogn)
O(mlogn), 通过反向索引堆优化,适合稠密图。
有关时间复杂度推导会在后面部分说明。
5. Dijkstra算法的核心原理
贪心思想: 始终选择当前源节点到未标记节点最短路径的顶点,然后遍历该顶点的边做松弛更新。 直到所有可达点均已被标记。
算法流程:
- 建图,(竞赛选择链式前向星, LC和小数据量可以邻接表, 顶点数较少选择邻接矩阵)
- 准备三个容器,
dist数组(int)
,visit数组(bool)
,最小堆(数组手写堆或者C++/Java内置的优先级队列)
。 - 初始化容器, dist数组值为 + ∞ +\infty +∞ ,visit数组为false, 创建或者清空堆。
- 主流程: 选择一个源节点
src
,dist[src]=0
。dist数组含义为src->target(目标节点)当前遍历到顶点的最短路径,visit数组表示当前顶点有无被访问过(用作记忆化搜索)将src加入到优先队列
。 - 优先队列(最小堆)内部优先级逻辑是(源节点到当前节点的距离,距离小的优先级更高), 如果当前优先队列不为空, 那么将这个顶点标记为已访问过执行后续步骤。 优先队列堆顶元素(记为
cur
)出队, 遍历它的所有邻居进行动态更新。 - 动态更新的逻辑: 若
v顶点未访问过
且dist[cur]+w<dist[v]
, w是cur->v的权值, v是cur的一个邻居。文字翻译为, 如果src到cur的最短路径+cur到v的最短路径小于src先前到v的最短路径, 那么更新这个值dist[v]=dist[cur]+w
, 并将 { v , d i s t [ v ] } \{v, dist[v]\} {v,dist[v]}加入到优先队列中。 - 5,6是个主循环, 当优先队列为空结束。 dist数组记录的就是src到所有可达节点的最短路径。 单源最短路径表
如果当前src到某个节点不可达, 由第3条它会被初始化为
∞
\infty
∞, 但由于不可达在第6条中它将不会得到更新。
因此,通过Dijkstra算法可以判断源节点到某一顶点之间是否可达。只需要查询dist表中是否为∞即可
代码实现
LC模板(C++示例)
前面已经给出了Dijkstra算法的文字描述, 只需要将其上述翻译成代码即可。
- 初始化步骤: 建图,初始化容器, 源节点的处理
- 主循环中处理优先队列动态更新的过程
先以LC上这道函数题为模板
链接点击这里
这道题可以很容易翻译为求当前源节点k到所有节点之间的最短距离。 该图是有向带权图, 权值是时间(必然为正数)。
以下以C++语言说明, 给定函数信息如下。
int networkDelayTime(vector<vector<int>>& times, int n, int k)
;
- 采用邻接表进行建图。
// 1. 建图:graph[u] 里存 (v, w)
vector<vector<pair<int, int>>> graph(n + 1);
for (auto& edge : times) {
int u = edge[0], v = edge[1], w = edge[2];
graph[u].push_back({v, w});
}
- 准备并初始化Dijkstra算法的visit和dist数组
// 2. 准备Dijkstra需要的数组
vector<int> dist(n + 1, INT_MAX);
vector<bool> visited(n + 1, false);
- 准备小根堆, 这里以C++标准库的优先级队列为例
// 3. 小根堆:存储 (节点, 当前距离)
auto cmp = [](const pair<int, int>& a, const pair<int, int>& b) {
// a.second > b.second => 小根堆
return a.second > b.second;
};
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(cmp)> minHeap(cmp);
- 流程开始, 处理源节点k
// 4. 初始化:从 k 出发
dist[k] = 0;
minHeap.push({k, 0});
- 主循环,终止条件:优先队列为空。
// 5. Dijkstra 主循环
while (!minHeap.empty()) {
auto node = minHeap.top().first;
minHeap.pop();
if (visited[node])
continue;
visited[node] = true;
// 遍历 node 的所有邻居
for (auto& [nxt, w] : graph[node]) {
if (!visited[nxt] && dist[node] + w < dist[nxt]) {
dist[nxt] = dist[node] + w;
minHeap.push({nxt, dist[nxt]});
}
}
}
- 本题特殊处理, 如果k节点存在不可达的顶点, 那么返回-1. 否则返回k节点到某一节点简单路径的最大值。
// 6. 统计结果:若有不可达节点,直接返回 -1;否则返回最大距离
int ans = 0;
for (int i = 1; i <= n; i++) {
if (dist[i] == INT_MAX) {
return -1;
}
ans = max(ans, dist[i]);
}
return ans;
C++完整实现
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
// 1. 建图:graph[u] 里存 (v, w)
vector<vector<pair<int, int>>> graph(n + 1);
for (auto& edge : times) {
int u = edge[0], v = edge[1], w = edge[2];
graph[u].push_back({v, w});
}
// 2. 准备Dijkstra需要的数组
vector<int> dist(n + 1, INT_MAX);
vector<bool> visited(n + 1, false);
// 3. 小根堆:存储 (节点, 当前距离)
auto cmp = [](const pair<int, int>& a, const pair<int, int>& b) {
// a.second > b.second => 小根堆
return a.second > b.second;
};
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(cmp)> minHeap(cmp);
// 4. 初始化:从 k 出发
dist[k] = 0;
minHeap.push({k, 0});
// 5. Dijkstra 主循环
while (!minHeap.empty()) {
auto node = minHeap.top().first; // 结构化绑定(C++17)
minHeap.pop();
if (visited[node])
continue;
visited[node] = true;
// 遍历 node 的所有邻居
for (auto& [nxt, w] : graph[node]) {
if (!visited[nxt] && dist[node] + w < dist[nxt]) {
dist[nxt] = dist[node] + w;
minHeap.push({nxt, dist[nxt]});
}
}
}
// 6. 统计结果:若有不可达节点,直接返回 -1;否则返回最大距离
int ans = 0;
for (int i = 1; i <= n; i++) {
if (dist[i] == INT_MAX) {
return -1;
}
ans = max(ans, dist[i]);
}
return ans;
}
};
Java完整实现
注释由ChatGPT生成。
同样邻接表进行建图
class Solution {
public int networkDelayTime(int[][] times, int n, int k) {
// 1. 建图:使用 ArrayList 存储邻接表,每个元素是一个 (目标节点, 权值) 数组
ArrayList<ArrayList<int[]>> graph = new ArrayList<>();
// 初始化 graph,使得下标从 0 到 n,共 n+1 个位置
for(int i = 0; i <= n; i++){
graph.add(new ArrayList<>());
}
// 2. 准备 Dijkstra 所需的数据结构
// dist[i] 存储从起点 k 到节点 i 的当前最短距离,初始化为最大值
int[] dist = new int[n + 1];
Arrays.fill(dist, Integer.MAX_VALUE);
// visit[i] 表示节点 i 是否已经确定了最短距离(是否被“收录”)
boolean[] visit = new boolean[n + 1];
// 3. 将输入的边 times[] 构建到 graph 中
// times[i] = [u, v, w] 表示边 u -> v,权重 w
// u,v,w 各自在代码中临时存储
int u, v, w;
for(int[] edge : times){
u = edge[0];
v = edge[1];
w = edge[2];
// 对应节点 u 的邻接列表中,加入 (v, w)
graph.get(u).add(new int[]{v, w});
}
// 4. 创建一个小顶堆 (最小优先队列),里面存储 (节点, 与起点距离)
// 依据距离从小到大排序
PriorityQueue<int[]> minheap = new PriorityQueue<>((a, b) -> a[1] - b[1]);
// 将起点 k 入队,初始距离为 0
minheap.add(new int[]{k, 0});
dist[k] = 0;
// 5. Dijkstra 主循环:
// 不断从最小堆中弹出距离最近的未访问节点,更新其邻居节点的距离
while(!minheap.isEmpty()){
// 取出堆顶元素 (u, 当前最小距离)
u = minheap.poll()[0];
// 如果该节点已被收录,跳过
if(visit[u]){
continue;
}
// 否则标记为已访问
visit[u] = true;
// 遍历节点 u 的所有邻居 (v, w)
for(int[] e : graph.get(u)){
v = e[0];
w = e[1];
// 若 v 尚未被收录,并且通过 u 到 v 的距离更短,则更新 dist[v]
if(!visit[v] && dist[u] + w < dist[v]){
dist[v] = dist[u] + w;
// 将更新后的 (v, dist[v]) 推入堆
minheap.add(new int[]{v, dist[v]});
}
}
}
// 6. 计算结果:找出 dist[] 数组中的最大值,即最慢到达时间
// 如果有节点仍是 Integer.MAX_VALUE,说明它不可达,返回 -1
int ans = Integer.MIN_VALUE;
for(int node = 1; node <= n; node++){
if(dist[node] == Integer.MAX_VALUE){
return -1; // 存在不可达节点
}
ans = Math.max(ans, dist[node]);
}
// 若所有节点可达,返回最大延迟时间
return ans;
}
}
普通堆优化下的Dijkstra算法模板
洛谷P4779
静态空间, 链式前向星建图,手写普通堆。
C++版本
#include<cstdio>
#include<climits>
using namespace std;
// ========== 常量定义 ==========
static const int MAXN = 1e5+10; // 节点上限(题意中一般为10^5)
static const int MAXM = 2e5+10; // 边的上限(题意中一般为2×10^5)
// ========== 链式前向星存图相关数组 ==========
int head[MAXN], nxt[MAXM], to_[MAXM], w[MAXM];
int cnt; // 当前边下标(从 1 开始)
// ========== Dijkstra 需要的数组 ==========`
int dist_[MAXN]; // dist_[u] 表示从起点 s 到 u 的最短距离++
bool visited[MAXN]; // visited[u] 表示节点 u 是否已经确定最短距离
// ========== 输入数据 ==========
int n, m, s; // n: 节点数, m: 边数, s: 起点
// ========== 初始化 ==========
void build() {
cnt = 1;
for(int i = 1; i <= n; i++){
head[i] = 0; // 没有边
dist_[i] = INT_MAX; // 最短距离初始为无穷
visited[i] = false;
}
}
// ========== 添加边 ==========
// 在链式前向星中,为 u 添加一条有向边 (u -> v),权重 wgt
void add_e(int u, int v, int wgt) {
nxt[cnt] = head[u];
to_[cnt] = v;
w[cnt] = wgt;
head[u] = cnt++;
}
// ========== 手写堆结构 ==========
// heap[i][0] 存节点编号, heap[i][1] 存该节点的距离
int heapArr[MAXM][2];
int hz; // 当前堆大小
// 交换堆元素
void swap_heap(int i, int j){
// 交换节点编号
int tmp = heapArr[i][0];
heapArr[i][0] = heapArr[j][0];
heapArr[j][0] = tmp;
// 交换距离
tmp = heapArr[i][1];
heapArr[i][1] = heapArr[j][1];
heapArr[j][1] = tmp;
}
// 上浮
void push_up(int i){
while(i > 0 && heapArr[i][1] < heapArr[(i - 1) / 2][1]){
swap_heap(i, (i - 1) / 2);
i = (i - 1) / 2;
}
}
// 下沉
void push_down(int i){
int l=2*i+1;
// 当 left 下标在堆内,说明有子节点
while(l < hz){
// 选出左右孩子中距离更小的
int best = (l+1 < hz && heapArr[l+1][1] < heapArr[l][1])
? l+1
: l;
//孩子和父亲比较
best = heapArr[best][1]<heapArr[i][1]?best:i;
//父亲优先级高, 那么终止下沉操作
if(best==i){
break;
}
swap_heap(best,i);
i = best;
l = 2*i+1;
}
}
// ========== Dijkstra (懒更新写法) ==========
void dijkstra() {
// 1) 起点距离设为 0
dist_[s] = 0;
// 2) 把起点 (节点 s, 距离 0) 放入堆
heapArr[0][0] = s;
heapArr[0][1] = 0;
hz = 1; // 初始堆大小=1
// 3) 循环
while(hz > 0){
// 取出堆顶
int u = heapArr[0][0];
// 把堆尾移到堆顶,堆大小-1
swap_heap(0, --hz);
push_down(0);
// 判断是否已访问
if(visited[u]) {
continue;
}
visited[u] = true;
// 松弛邻居
for(int ei = head[u]; ei; ei = nxt[ei]) {
int v = to_[ei];
int wgt = w[ei];
// 如果可以更新 dist_[v]
if(!visited[v] && dist_[u] + wgt < dist_[v]) {
dist_[v] = dist_[u] + wgt;
// 入堆
heapArr[hz][0] = v;
heapArr[hz][1] = dist_[v];
push_up(hz);
hz++;
}
}
}
}
// ========== 主函数 ==========
int main(){
// 读入
scanf("%d %d %d", &n, &m, &s);
build();
// 有向边输入
for(int i = 0; i < m; i++){
int u, v, wgt;
scanf("%d %d %d", &u, &v, &wgt);
// 只加一次 => 有向图
add_e(u, v, wgt);
}
// Dijkstra
dijkstra();
// 输出 dist_[1]..dist_[n]
// 注:题目节点从1..n,这里一定要打印 dist_[1]
printf("%d", dist_[1]);
for(int i=2; i <= n; i++){
printf(" %d", dist_[i]);
}
puts("");
return 0;
}
java版本
import java.io.BufferedReader;
import java.io.StreamTokenizer;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.IOException;
//测试链接:https://www.luogu.com.cn/problem/P4779
//提交时修改主类名为Main
public class Dijkstra {
public static final int MAXN = (int)1e5 + 10;
public static final int MAXM = (int)2e5 + 10;
// ========== 读取输入相关 ==========
public static int n, m, s; // n:节点数, m:边数, s:起点
// ========== 链式前向星存图 ==========
public static int[] head = new int[MAXN];
public static int[] next = new int[MAXM];
public static int[] to = new int[MAXM];
public static int[] weight = new int[MAXM];
public static int cnt;
// ========== Dijkstra 需要的辅助 ==========
public static int[] dist = new int[MAXN];
public static boolean[] visited = new boolean[MAXN];
// ========== 小顶堆结构 (heapArr[i][0] = 节点编号, heapArr[i][1] = 距离) ==========
public static int[][] heapArr = new int[MAXM][2];
public static int heapSize;
// ========== 初始化 ==========
public static void build() {
cnt = 1;
for(int i = 0; i <= n; i++){
head[i] = 0;
visited[i] = false;
dist[i] = Integer.MAX_VALUE;
}
heapSize = 0;
}
// ========== 添加一条 (u->v) 的有向边,权重 wgt ==========
public static void addEdge(int u, int v, int wgt){
next[cnt] = head[u];
to[cnt] = v;
weight[cnt] = wgt;
head[u] = cnt++;
}
// ========== 交换堆中两个位置 ==========
public static void swap(int i,int j){
int[] tmp = heapArr[i];
heapArr[i] = heapArr[j];
heapArr[j] = tmp;
}
// ========== “上浮”操作:对下标 i 的元素进行向上调整 ==========
public static void push_up(int i){
while(heapArr[i][1] < heapArr[(i - 1) / 2][1]){
swap(i, (i - 1) / 2);
i = (i - 1) / 2;
}
}
// ========== “下沉”操作:对下标 i 的元素进行向下调整 ==========
//这里实际上仅对堆顶做下沉操作, 故而可以如下方式写
public static void push_down(){
int i = 0;
int l = 1;
while(l < heapSize){
// 先比较左右孩子,选出更小的
int r = l + 1;
int best = (r < heapSize && heapArr[r][1] < heapArr[l][1])
? r : l;
// 再与父节点比较
if(heapArr[best][1] < heapArr[i][1]){
swap(best, i);
i = best;
l = i * 2 + 1;
} else {
break;
}
}
}
// ========== Dijkstra (懒更新写法) ==========
public static void dijkstra(){
// 1) 起点距离设为0,堆中插入 (s, 0)
dist[s] = 0;
heapArr[0][0] = s;
heapArr[0][1] = 0;
heapSize = 1;
// 2) 主循环
while(heapSize > 0){
// 取出堆顶 (u, curDist)
int u = heapArr[0][0];
// 移除堆顶 => 将堆尾移到顶,再“下沉”
swap(0, --heapSize);
if(heapSize > 0) {
push_down(0);
}
// 如果已访问过,跳过
if(visited[u]) {
continue;
}
// 标记 u 已确定最短距离
visited[u] = true;
// 3) 用 u 去松弛邻居 v
for(int ei = head[u]; ei > 0; ei = next[ei]){
int v = to[ei];
int wgt = weight[ei];
// 若可以松弛
if(!visited[v] && dist[u] + wgt < dist[v]){
dist[v] = dist[u] + wgt;
// 插入到堆的末尾,令其“上浮”
heapArr[heapSize][0] = v;
heapArr[heapSize][1] = dist[v];
push_up(heapSize);
heapSize++;
}
}
}
}
// ========== 主函数 ==========
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
// 如果题目有多组测试,就用 while(in.nextToken() != ...) 包一层循环
while(in.nextToken() != StreamTokenizer.TT_EOF){
n = (int)in.nval;
// 初始化
build();
in.nextToken(); m = (int)in.nval;
in.nextToken(); s = (int)in.nval;
// 读入 m 条边
for(int i=0,u,v,wgt; i < m; i++){
in.nextToken(); u = (int)in.nval;
in.nextToken(); v = (int)in.nval;
in.nextToken(); wgt = (int)in.nval;
addEdge(u, v, wgt);
}
// 执行 Dijkstra
dijkstra();
// 输出 dist[1..n]
out.print(dist[1]);
for(int i = 2; i <= n; i++){
out.print(" " + dist[i]);
}
out.println();
}
out.flush();
br.close();
out.close();
}
}
正确性
以下是便于理解的大致思路, 并非严格的数学推导证明笔者不会。
1. 贪心选择的核心思想
D
i
j
k
s
t
r
a
Dijkstra
Dijkstra是基于贪心思想实现。
核心是:在每一轮中选择当前距离源点最近的、尚未确定最短路径的节点
u
u
u,并确定它的最短路径值。
保证以下两点
- 对于已访问到的节点, 源点到它的最短路径确定了。
- d i s t [ u ] dist[u] dist[u]是所有未确定节点中最小的,且从源点 s r c src src 到 u u u的路径不会被更新为更短。
2. 为什么未访问的节点一定不优于已访问的节点?
在算法的每一轮:
- dis[u]最小的节点u从优先队列中取出。此时,所有未访问节点的 d i s [ v ] dis[v] dis[v] 值要么等于其实际最短路径(但未被访问到),要么是尚未更新的更大值。
- 因为 u u u是当前 d i s dis dis最小的节点,且边权为非负,其他未访问节点 v v v不可能通过任意路径更新为比 d i s [ u ] dis[u] dis[u]更小的值。
因此:
d
i
s
[
v
]
≥
d
i
s
[
u
]
dis[v] \geq dis[u]
dis[v]≥dis[u]
这保证了每轮选择出的节点
u
u
u的最短路径已经确定。
3. 路径(懒)更新和边权非负性
- Dijkstra 算法的贪心性质依赖于边权非负。
- 这是因为,如果边权是非负的,则已访问节点的最短路径值 ( dis[u] ) 不可能通过任何路径再被降低。
- 若尝试通过 ( u ) 更新其邻居 ( v ) 的路径:
d i s [ v ] = min ( d i s [ v ] , d i s [ u ] + w ( u , v ) ) dis[v] = \min(dis[v], dis[u] + w(u, v)) dis[v]=min(dis[v],dis[u]+w(u,v))
dis[v]要么降低要么会被更新得更小,但不会影响已确定 u u u节点的最短路径。
4. 初始条件成立
如果说2,3条保证循环的成立,那么第4条对初始化的成立保证一开始的初始条件成立。
通过贪心选择和路径更新,Dijkstra 逐步确定了从源点 ( s ) 到每个节点的最短路径:
- 初始时,只有源点的最短路径值为0,其他所有节点的路径值为 ∞ \infty ∞。
- 根据第2,3条保证后续未访问节点的最短路径一定更新正确。
总结
“算法之美在于解决问题的简洁性与优雅性,而Dijkstra算法正是其中的典范之一.”
有关最短路径算法的题目, 有时间再写吧。