1、算法思路
1. 解决的问题
Dijkstra算法是基于贪心的一种算法, 其目的是解决正权值图的单源最短路问题。
所谓正权值,就是指图中所有边的权值为正,所谓单源最短路就是从某一个起点出发到图中任一个位置的最短路。
在使用Dijkstra算法时,如果是稀疏图(边的数量级小于点数的平方),那么最好使用堆优化版的Dijkstra,如果是稠密图也就是(m ~ n²)那么可以继续使用朴素Dijkstra算法。
2. 具体的小例子
空说其做法不太直观,不如先看个小例子。
初始状态下,除了起点之外,其他所有点到起点的最短距离均设置为正无穷。
从所有点中(包括起点)找到一个没有作为距离起点最近的点更新过其他节点的(被标记)并且距离起点最近的点,显然在当前的情况下是起点自己,本例中也就是4号点。利用选出的这个距离起点最近的点,更新所有没有被标记且能该节点能到达的点(1,2,3)到到起点的距离。具体的更新做法是:
例如对于节点3,将3当前到起点的最短距离(正无穷)与3与这个距离起点最近的点的距离加上这个距离起点最近的点到起点的距离进行比较,选出较短的一个进行更新。这里也就是 正无穷和1 (3到4)+ 0(4到4)进行比较,结果就是将3更新为1,其余节点同理。更新结束后,将4号点进行标记。
第二次迭代同理,从所有点中(包括起点)找到一个没有作为距离起点最近的点更新过其他节点的(也就是被标记的点)并且距离起点最近的点。这里起点已经做过这个点了,所以我们选择3号点。并利用3号节点对它能到达并且没有被标记的点进行更新。这里就只有2号节点,将2到起点的最短距离3与2号到3号节点的距离加上3号节点到起点的距离进行比较(5和2 + 1进行比较)将2的最短距离更新为3。
就这样进行迭代,直到所有节点都被标记,则获得了起点到所有其他点的最短距离。
3. 堆优化版Dijkstra算法
我们将上面的朴素Dijkstra算法的伪代码书写如下
for i: 1- n
t <- 未被标记的,距离起点最近的点 步骤1
将t标记 步骤2
用t去更新其他点的距离 步骤3
朴素的Dijkstra算法的时间复杂度
O
(
n
2
)
O(n^2)
O(n2)
其中,每次循环步骤1总共要执行
n
2
n^2
n2次(n为点数),所有循环步骤3总共执行m次(m指边数,使用邻接表存储,每个t都只会它能达到的点,所有的t总共加起来就是所有的边数)。可以看出,最费时间的步骤是步骤1,而步骤1是为了找一堆数字里最小的一个,那么我们可以使用堆来对这个步骤进行优化。
这样在步骤1只需要使用O(1)的时间就可以找到最小值,而在步骤3进行调整的时候,我们知道堆进行调整的时间复杂度为O(logn)的,而步骤3总共会执行m次,所以堆优化版的Dijkstra算法时间复杂度会变成
O
(
m
l
o
g
n
)
O(mlogn)
O(mlogn)
所以在使用Dijkstra算法时,如果是稀疏图(边的数量级小于点数的平方),那么最好使用堆优化版的Dijkstra,如果是稠密图也就是(m ~ n²)那么可以继续使用朴素Dijkstra算法。
2、例题
下面两个例题分别演示朴素Dijkstra算法及其堆优化版本
1.Dijkstra求最短路 I
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 −1。
数据范围
1≤n≤500,
1≤m≤10^5,
图中涉及边长均不超过10000。
输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
可以看到,这里是稠密图,所以我们使用朴素的算法
#include<iostream>
#include<cstring>
using namespace std;
const int N = 1e5 + 10;
int e[N],ne[N],h[N],idx,w[N];
int d[510],n,m;
bool st[510];
void add(int a, int b, int v)
{
e[idx] = b;
ne[idx] = h[a];
w[idx] = v;
h[a] = idx ++;
}
int dj()
{
memset(d,0x3f,sizeof d);
d[1] = 0;
for(int i = 0; i < n; i ++)
{
int u = -1, min = 0x3f3f3f3f;
for(int i = 1; i <= n; i ++)
{
if(d[i] < min && !st[i])
{
min = d[i];
u = i;
}
}
if(u == -1)break;
st[u] = true;
for(int i = h[u]; i != -1; i = ne[i])
{
if(d[e[i]] > d[u] + w[i])
{
d[e[i]] = d[u] + w[i];
}
}
}
if(d[n] == 0x3f3f3f3f)return -1;
else return d[n];
}
int main()
{
cin >> n >> m;
memset(h,-1,sizeof h);
for(int i = 0; i < m; i ++)
{
int a,b,v;
cin >> a >> b >> v;
add(a,b,v);
}
cout << dj();
}
2.Dijkstra求最短路II
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 −1。
数据范围
1≤n,m≤1.5×10^5,
图中涉及边长均不小于 0,且不超过 10000。
输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N = 2e5 + 10;
typedef pair<int,int> PII;
int e[N],ne[N],idx,h[N],w[N],d[N],n,m;
bool st[N];
void add(int a, int b, int x)
{
e[idx] = b;
ne[idx] = h[a];
w[idx] = x;
h[a] = idx ++;
}
int dj_h()
{
memset(d,0x3f,sizeof d);
d[1] = 0;
priority_queue<PII,vector<PII>, greater<PII>> q;
q.push({0,1});
while(q.size())
{
auto t = q.top();
int s = t.second;
q.pop();
if(st[s])continue;
else st[s] = true;
for(int i = h[s]; i != -1; i = ne[i])
{
if(d[e[i]] > d[s] + w[i])
{
d[e[i]] = d[s] + w[i];
q.push({d[e[i]],e[i]});
}
}
}
if(d[n] == 0x3f3f3f3f) return -1;
else return d[n];
}
int main()
{
cin >> n >> m;
memset(h,-1,sizeof h);
for(int i = 0; i < m; i ++)
{
int a,b,x;
cin >> a >> b >> x;
add(a,b,x);
}
int ans = dj_h();
cout << ans;
}
参考资料
- 数据结构与算法分析C语言描述(第二版)9.3.2
- Acwing