定义图有:n个点,m条边
Dijkstra是处理单源最短路的算法,要求边权为正。
1、朴素Dijkstra O(n^2)
1.1、算法步骤
假设起点是st;已经确定到st最短距离的点在集合S中
- d i s [ s t ] = 0 , ( i ≠ s ) d i s [ i ] = + ∞ dis[st] = 0, (i\neq s)dis[i] = +\infty dis[st]=0,(i=s)dis[i]=+∞
for(i = 0; i < n; i ++ )
2.1. 不在S中,距离st最近的点x
2.2. x放入S
2.3. 用x更新它连的所有点y(松弛操作):
if(dis[y] > dis[x] + w(x, j)) dis[j] = dis[x] + w(x, y)
第二步循环内每次都能确定一个点x的最短距离,重复n次就能确定所有点的最短距离。
以算法导论的图为例:
a. 初始化dis[s]=0
b. 到s最近的就是s本身,更新s连的y和t,
S
=
{
s
}
S=\{s\}
S={s}
c. 到s最近的是y(
y
:
5
<
t
:
10
<
x
:
∞
=
z
:
∞
y:5 < t:10<x:\infty=z:\infty
y:5<t:10<x:∞=z:∞),更新y连的t、x、z,
S
=
{
s
,
y
}
S=\{s,y\}
S={s,y}
d. 到s最近的是z(
z
:
7
<
t
:
8
<
x
:
14
z:7 < t:8 < x:14
z:7<t:8<x:14),更新z连的x,
S
=
{
s
,
y
,
z
}
S=\{s,y,z\}
S={s,y,z}
e. 到s最近的是t(
t
:
8
<
x
:
13
t:8 < x:13
t:8<x:13),更新t连的x,
S
=
{
s
,
y
,
z
,
t
}
S=\{s,y,z,t\}
S={s,y,z,t}
f. 到s最近的是x(只剩x了),结束。
1.2、算法证明
粗略证明
- 证明目标:每个进S的点由算法得到的距离都是实际最短路的距离。
- 如果某个点(下图中的u)由算法得到的答案不是最短路,那在理论最短路径中,u肯定是连在“比u晚进S的点”(下图中的y)后面。
1.1. 因为如果是连在"比u先进入S的点"(下图中的x)后面,由归纳得x是最短路,x能松弛到u,算法得到得答案也是理论最短路径,矛盾。 - 而路径中y在u前面,边权又都是正的,算法是挑近的进S,y肯定会比u更早进S,矛盾。
- 因此不存在u,进S的点由算法得到的距离就是实际最短路的值。
严格证明(参考算法导论)
假设u之前的点,有性质:对于所有加入S的点,在这个点加入S时,根据算法的步骤所确定的st到这个点的距离,都是最短路。
记:根据算法的步骤所确定的 s t st st到点 i i i的距离 = d i s [ i ] dis[i] dis[i]; s t st st到点 i i i的实际最短路= a n s [ i ] ans[i] ans[i]
那么性质可简写为:对于所有加入S的点 i i i,在 i i i加入S时,都有 d i s [ i ] = a n s [ i ] dis[i]=ans[i] dis[i]=ans[i]。
命题如下:点
u
u
u加入S时,
u
u
u是第一个使这个性质不成立的点,即
d
i
s
[
u
]
≠
a
n
s
[
u
]
dis[u] \neq ans[u]
dis[u]=ans[u]
利用反证法证明不存在这样的点 u u u:
- u u u和 s t st st不同:因为 s t st st是第一个进入集合 S S S的结点, d i s [ s t ] = 0 dis[st]=0 dis[st]=0,是最短路,性质成立。
- 此时 S S S不是空集: u u u加进来时至少有点 s t st st
- 此时已经存在某条从 s t st st到 u u u的路径:算法每次都是找距离 s t st st最近的点,如果不存在 s t st st到 u u u的路径,那么 d i s [ u ] = ∞ dis[u]=\infty dis[u]=∞, u u u不可能被加进来。
- 不妨设目前所有
s
t
st
st到
u
u
u的路径中,最短的路径是 st —p1—> x —> y —p2—> u,
x
x
x是
y
y
y的前一个点,
y
y
y是第一个不属于
S
S
S的点,因此
x
x
x在
S
S
S中。
ps. (p1和p2可能不包含任何边,可能 s t = x st=x st=x或 y = u y=u y=u,但 x x x和 y y y不同,p2可能重新进入 S S S) - d i s [ x ] = a n s [ x ] dis[x]=ans[x] dis[x]=ans[x]:因为 u u u是第一个使这个性质不成立的点,而 x x x在 u u u之前且已经进入 S S S中, x x x满足性质。
-
d
i
s
[
y
]
=
a
n
s
[
y
]
dis[y]=ans[y]
dis[y]=ans[y]:点
x
x
x进入
S
S
S时,进行松弛更新了
y
y
y,得到了
d
i
s
[
y
]
dis[y]
dis[y]。
而 y y y在 s t st st—> u u u的最短路上,如果还存在其他st—p3—>y的最短路,那么st —p3—> y —p2—> u,才是st—>u的最短路,这和第4步的假设矛盾。
因此st —p1—> x —> y就是st—>y的最短路径。(最短路径的子路径也是最短路径) - a n s [ y ] ≤ a n s [ u ] ans[y]\leq ans[u] ans[y]≤ans[u]:因为 y y y在 u u u之前,且所有路径都是正的。
- a n s [ u ] ≤ d i s [ u ] ans[u]\leq dis[u] ans[u]≤dis[u]:算法估计的最短路只可能比真实的最短路长。(上界性质)
- d i s [ u ] ≤ d i s [ y ] dis[u]\leq dis[y] dis[u]≤dis[y]:u和y都不在S中,而算法每次都会从不在S中的点里面挑距离 s t st st最近的,也就是dis更小的,而此时 u u u正准备进 S S S,而 y y y是在之后才进 S S S。因此 d i s [ u ] ≤ d i s [ y ] dis[u]\leq dis[y] dis[u]≤dis[y]。
- 根据第7、8、9步,我们得到
a
n
s
[
y
]
≤
a
n
s
[
u
]
≤
d
i
s
[
u
]
≤
d
i
s
[
y
]
ans[y]\leq ans[u] \leq dis[u] \leq dis[y]
ans[y]≤ans[u]≤dis[u]≤dis[y]
由根据第6步的 d i s [ y ] = a n s [ y ] dis[y]=ans[y] dis[y]=ans[y],有 a n s [ y ] = a n s [ u ] = d i s [ u ] = d i s [ y ] ans[y] = ans[u] = dis[u] = dis[y] ans[y]=ans[u]=dis[u]=dis[y]
可以看到 a n s [ u ] = d i s [ u ] ans[u] = dis[u] ans[u]=dis[u]和命题矛盾 - 综上不存在这样的点 u u u,因此算法所求的所有点都满足性质,即进入S的点所确定的距离都是最短路。
1.3、算法实现
题目链接
这题70%的数据1≤n≤1000,n较小可采用邻接矩阵,对于n较大可采用邻接链表。
遇到重边时,采用邻接矩阵需要取最小的边,采用邻接链表则不需要考虑。
// 邻接矩阵
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1005;
int n, m, dis[N], st;
int g[N][N];
bool flag[N]; // 该点最短路是否已经被确定(进入集合S)
void dijkstra() {
memset(dis, 0x3f, sizeof dis);
memset(flag, 0, sizeof flag);
dis[st] = 0;
for (int i = 0; i < n; i ++ ) {
int x = -1; // 没确定最短路中离起点最近的点
for (int j = 1; j <= n; j ++ ) {
if (!flag[j] && (x == -1 || dis[x] > dis[j])) x = j;
}
flag[x] = true;
for (int j = 1; j <= n; j ++ ) {
dis[j] = min(dis[j], dis[x] + g[x][j]);
}
}
}
int main() {
scanf("%d%d%d", &n, &m, &st);
memset(g, 0x3f, sizeof g);
while(m -- ) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
g[u][v] = min(g[u][v], w); // 重边取最小的边
}
dijkstra();
for (int i = 1; i <= n; i ++ ) {
if (dis[i] == 0x3f3f3f3f) printf("2147483647 ");
else printf("%d ", dis[i]);
}
return 0;
}
2、堆优化Dijkstra O(mlogn)
当n较大时,如1≤n≤100000,那么
O
(
n
2
)
O(n^2)
O(n2)的算法将会超时,需要用邻接链表+堆优化。
回顾Dijkstra的流程:
- d i s [ s t ] = 0 , ( i ≠ s ) d i s [ i ] = + ∞ dis[st] = 0, (i\neq s)dis[i] = +\infty dis[st]=0,(i=s)dis[i]=+∞
for(i = 0; i < n; i ++ )
2.1. 不在S中,距离st最近的点x
2.2. 用x更新它连的所有点y(松弛操作):
if(dis[y] > dis[x] + w(x, j)) dis[j] = dis[x] + w(x, y)
2.3. x放入S
其中2.1可以采用小根堆维护
d
i
s
[
i
]
dis[i]
dis[i],直接挑出距离
s
t
st
st最近的点,而不需要遍历所有点。
这样2.1的时间就变成
O
(
1
)
O(1)
O(1),但是堆的修改操作时间复杂度是
O
(
l
o
g
n
)
O(logn)
O(logn),整个算法至多需要修改
m
m
m条边(松弛会用到每个点的所有边,所有点都要进行松弛操作)。
直到堆为空停止,而不是采用for循环。
因此时间复杂度是
O
(
m
l
o
g
n
)
O(mlogn)
O(mlogn)
注意如果用STL的优先队列,由于无法修改元素,只能往里面插新的元素,因此时间复杂度可能变成 O ( m l o g m ) O(mlogm) O(mlogm),但是 m ≤ n 2 m\leq n^2 m≤n2, l o g m ≤ 2 l o g n logm\leq 2logn logm≤2logn,因此 O ( m l o g m ) O(mlogm) O(mlogm)等价于 O ( m l o g n ) O(mlogn) O(mlogn)
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 100005;
const int M = 500005;
int n, m, dis[N], st;
int head[N], nxt[M], lnk[M], val[M], idx;
bool flag[N]; // 该点最短路是否已经被确定(进入集合S)
void add(int u, int v, int w) {
lnk[idx] = v;
val[idx] = w;
nxt[idx] = head[u];
head[u] = idx ++ ;
}
void dijkstra() {
memset(dis, 0x3f, sizeof dis);
memset(flag, 0, sizeof flag);
priority_queue<PII, vector<PII>, greater<PII> > heap; // 小根堆,存{dis[x], 结点x}
heap.push({0, st}); // 堆默认用pair的first排序
dis[st] = 0;
while (heap.size()) {
auto t = heap.top();
heap.pop();
int x = t.second;
if (flag[x]) continue;
flag[x] = true;
for (int i = head[x]; i != -1; i = nxt[i]) {
int y = lnk[i];
int w = val[i];
if (dis[y] > dis[x] + w) {
dis[y] = dis[x] + w;
heap.push({dis[y], y});
}
}
}
}
int main() {
scanf("%d%d%d", &n, &m, &st);
memset(head, -1, sizeof head);
while(m -- ) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
add(u, v, w);
}
dijkstra();
for (int i = 1; i <= n; i ++ ) {
if (dis[i] == 0x3f3f3f3f) printf("2147483647 ");
else printf("%d ", dis[i]);
}
return 0;
}