【刷题】图论——最短路:Dijkstra【证明+模板】


定义图有:n个点,m条边

Dijkstra是处理单源最短路的算法,要求边权为正。

1、朴素Dijkstra O(n^2)

1.1、算法步骤

假设起点是st;已经确定到st最短距离的点在集合S中

  1. 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]=+
  2. 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、算法证明

粗略证明

  1. 证明目标:每个进S的点由算法得到的距离都是实际最短路的距离。
  2. 如果某个点(下图中的u)由算法得到的答案不是最短路,那在理论最短路径中,u肯定是连在“比u晚进S的点”(下图中的y)后面。
    1.1. 因为如果是连在"比u先进入S的点"(下图中的x)后面,由归纳得x是最短路,x能松弛到u,算法得到得答案也是理论最短路径,矛盾。
  3. 而路径中y在u前面,边权又都是正的,算法是挑近的进S,y肯定会比u更早进S,矛盾。
  4. 因此不存在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

  1. 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,是最短路,性质成立。
  2. 此时 S S S不是空集: u u u加进来时至少有点 s t st st
  3. 此时已经存在某条从 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不可能被加进来。
  4. 不妨设目前所有 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)
  5. 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满足性质。
  6. 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的最短路径。(最短路径的子路径也是最短路径)
  7. a n s [ y ] ≤ a n s [ u ] ans[y]\leq ans[u] ans[y]ans[u]:因为 y y y u u u之前,且所有路径都是正的。
  8. a n s [ u ] ≤ d i s [ u ] ans[u]\leq dis[u] ans[u]dis[u]:算法估计的最短路只可能比真实的最短路长。(上界性质)
  9. 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]
  10. 根据第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]和命题矛盾
  11. 综上不存在这样的点 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的流程:

  1. 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]=+
  2. 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 mn2 l o g m ≤ 2 l o g n logm\leq 2logn logm2logn,因此 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; 
} 
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值