一、最短路几种算法的对比
LeetCode相关题目:
743. 网络延迟时间
847. 访问所有节点的最短路径
二、Dijkstra
1.基本思路
稠密图用邻接矩阵,稠密图(m约为n2)用朴素Dijkstra:
初始化距离dist[1] = 0, dist[i] = +无穷
// Si:当前已经确定的最短距离的点集
For i :1~n O(n)
t <- 不在s中的距离最近的点 //O(n),稀疏图可堆优化为O(1)
s <- t //每次t都不一样
用t更新其他点的距离: //总共O(n^2)约为O(m) 稀疏图可优化为总共O(mlogn)
时间复杂度 O ( n 2 ) O(n^2) O(n2) ,不妨只考虑有向图。
稀疏图用邻接表,稀疏图(m约为n)用堆优化版Dijkstra。时间复杂度
O
(
m
l
o
g
n
)
O(mlogn)
O(mlogn)。
总之,Dijkstra是一种贪心算法。
2.定理/规律
定理(最优子结构特征):
若图G不存在负有向圈,则任一最短路的子路也是相应点对间的最短路。
证明:
可容易由反证法证得。
规律:
根据该算法每次得到的点u对应的最短距离d(u)
是递增的。
证明:
设
S
S
S为已确定最短路的点的集合,
S
S
S中依次确定的点为
u
0
,
u
1
,
.
.
.
,
u
n
−
1
u_0,u_1,...,u_{n-1}
u0,u1,...,un−1,
d
[
u
]
d[u]
d[u]为
S
S
S里点
u
u
u的最短距离,
d
i
s
t
[
j
]
dist[j]
dist[j]为
S
S
S补集里点
j
j
j的当前距离。
考虑第
k
k
k个得到的点
u
k
u_k
uk的产生过程,其最短距离为
d
(
u
k
)
d(u_k)
d(uk)。对于
S
k
−
1
=
{
u
0
,
.
.
.
,
u
k
−
1
}
S_{k-1}= \left\{ {u_0,...,u_{k-1}} \right\}
Sk−1={u0,...,uk−1},在
S
k
−
1
S_{k-1}
Sk−1所更新的所有
d
i
s
t
dist
dist中,
d
(
u
k
)
d(u_k)
d(uk)为最小因而被选中,而剩余的
d
i
s
t
dist
dist都大于
d
(
u
k
)
d(u_k)
d(uk)。… … … … … … … …(1)
此时,新的
S
k
=
S
k
−
1
∪
{
u
k
}
S_k=S_{k-1} \cup \left\{ {u_k} \right\}
Sk=Sk−1∪{uk}
此时再去更新
d
i
s
t
dist
dist时,只有
u
k
u_k
uk能改变其他点的
d
i
s
t
dist
dist数值(
S
k
−
1
S_{k-1}
Sk−1能改变的
d
i
s
t
dist
dist已在产生
u
k
u_{k}
uk时改变). … … … … … … … … …(2)
因此由(1)(2)可知,
d
i
s
t
dist
dist均大于
d
(
u
k
)
d(u_{k})
d(uk).
这时,再从所有
d
i
s
t
dist
dist中选最小的作为
d
(
u
k
+
1
)
d(u_{k+1})
d(uk+1),则
d
(
u
k
+
1
)
d(u_{k+1})
d(uk+1)必大于
d
(
u
k
)
d(u_{k})
d(uk).
因此,结论成立。
3.算法正确性证明
Dijkstra的正确性
即每一步得到的d[u]都是起点到结点u的最短路径值。
证明:
用数学归纳法,显然当k=0,1时结论成立。
假设当
n
<
k
n<k
n<k时结论都成立,即
u
0
,
u
1
,
.
.
.
,
u
k
−
1
u_0,u_1,...,u_{k-1}
u0,u1,...,uk−1都找到了最短路,最短距离分别为
d
0
,
d
1
,
.
.
.
,
d
k
−
1
d_0,d_1,...,d_{k-1}
d0,d1,...,dk−1。
则当
n
=
k
n=k
n=k时,若所确定的
d
k
d_{k}
dk非真实最优解,则存在另一条到达
u
k
u_k
uk的路径P,P为真实最短路,使得该路径长度
d
p
<
d
k
d_p<d_{k}
dp<dk,且设该路径在S中的最后一个点为
u
t
u_t
ut。
(1)若
u
t
u_t
ut不为
u
k
−
1
u_{k-1}
uk−1,则由定理(最优子结构特征)与归纳假设知,最短路P的距离
d
p
=
d
t
+
c
o
s
t
(
t
,
k
)
d_p=d_t+cost(t, k)
dp=dt+cost(t,k),其中
c
o
s
t
(
t
,
k
)
cost(t, k)
cost(t,k)为
u
t
u_t
ut与
u
k
u_k
uk的最短路距离,可能包含多条边的权重。由于
d
k
=
m
i
n
{
d
i
+
w
(
i
,
k
)
}
,
i
=
0
,
1
,
.
.
.
,
k
−
1.
d_k=min \left\{ {di + w(i, k)} \right\},i=0,1,...,k-1.
dk=min{di+w(i,k)},i=0,1,...,k−1.知,
d
k
<
d
p
d_k<d_p
dk<dp,因此矛盾;
(2)若
u
j
u_j
uj为
u
k
−
1
u_{k-1}
uk−1,则存在路使得
d
k
−
1
d_{k-1}
dk−1更小,与归纳假设矛盾。
综上,命题成立。
三、朴素Dijkstra求最短路算法
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 −1。
数据范围
1≤n≤500,
1≤m≤105,
图中涉及边长均不超过10000。
输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
思路
由n,m的数据范围可知,该图为稠密图,用朴素Dijkstra。
代码
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510;
int n, m;
int g[N][N]; // 用邻接矩阵
int dist[N]; // 距离
bool st[N]; // 标记是否在S集合中
int dijkstra()
{
memset(dist, 0x3f, sizeof dist); // 初始化距离为无穷
dist[1] = 0; // 起点
for(int i = 0; i < n; i ++ ) // 确定n个点共需要n步
{
int t = -1;
for(int j = 1; j <= n; j ++ )//更新后取最小或第一次取起点
if(!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
st[t] = true; // 放入集合S中
for(int j = 1; j <= n; j ++ ) // 用t去更新
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
if(dist[n] == 0x3f3f3f3f) return -1; // 路径不存在
return dist[n];
}
int main()
{
scanf("%d%d", &n, &m);
memset(g, 0x3f, sizeof g);
while(m --)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
g[a][b] = min(g[a][b], c);
}
int t = dijkstra();
printf("%d\n", t);
return 0;
}
四、堆优化版Dijkstra
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 −1。
数据范围
1≤n,m≤1.5×105,
图中涉及边长均不小于 0,且不超过 10000。
输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
思路
由n,m的数据范围知,该图为稀疏图,用堆优化的Dijkstra。
由于每次确定出的点t是不同的,用t去更新,则总共需更新O(m)次,每次更新在堆中的复杂度为O(logn),因此更新这步的总时间复杂度为O(mlogn)。取最小需进行O(n)次,每次取最小在堆中的时间复杂度为O(1),因此取最小这步的时间复杂度为O(n)。
故堆优化版的Dijkstra的时间复杂度为O(mlogn)。
tips
朴素版未必就性能差,堆优化也未必性能好,主要看图的稀疏程度。
(1)稀疏图的
n
n
n与
m
m
m相似,因此堆优化版的时间复杂度
O
(
m
l
o
g
n
)
≈
O
(
m
l
o
g
m
)
O(mlogn) \approx O(mlogm)
O(mlogn)≈O(mlogm)。
O
(
m
l
o
g
m
)
<
O
(
m
∗
m
)
O(mlogm)<O(m*m)
O(mlogm)<O(m∗m),因此稀疏图用堆优化。
(2)稠密图的
m
m
m大致为
n
2
n^2
n2级别。
O
(
n
2
)
≈
O
(
m
)
<
O
(
m
l
o
g
n
)
O(n^2) \approx O(m) < O(mlogn)
O(n2)≈O(m)<O(mlogn),因此稠密图用朴素版。
代码
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 150010;
// 稀疏图用邻接表来存
int n, m;
int h[N], e[N], ne[N], w[N], idx; // w用来存边权重
int dist[N]; // 用来存距离
bool st[N]; // 如果为true说明这个点的最短路径已经确定
/*有重边也不要紧,假设1->2有权重为2和3的边,再遍历到点1的时候2号点的距离会更新两次放入堆中,
这样堆中会有很多冗余的点,但是在弹出的时候还是会弹出最小值2+x(x为之前确定的最短路径),并
标记st为true,所以下一次弹出3+x会continue不会向下执行。*/
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap; // 定义一个小根堆
heap.push({0, 1});
while(heap.size())
{
auto t = heap.top(); // 取不在集合S中距离最短的点
heap.pop();
int ver = t.second, distance = t.first;
if(st[ver]) continue;
//去冗余,1.重边2.更新后dist更短放入堆中,堆中有原有的冗余dist
st[ver] = true;
for(int i = h[ver]; i != -1; i = ne[i])
{
int j = e[i]; // i只是个下标,e中在存的是i这个下标对应的点。
if(dist[j] > distance + w[i])
{
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
if(dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while(m --)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
int t = dijkstra();
printf("%d\n", t);
return 0;
}