【知识点2】最短路径概述 和 Dijkstra算法⭐⭐⭐⭐⭐


今天也是为了cc,努力奋斗的一天ヾ(≧▽≦*)o

1. 引言

什么是最短路径问题(What)

对于任意给出的图G(V,E)和起点S、终点T,求从ST的权值最小的路径。

解决最短路径问题的方法有哪些(How)

  1. Dijkstra算法——求解单源最短路径,而且不能带负数的权!!!
  2. Bellman-Ford算法
  3. SPFA算法
  4. Floyd算法
  5. 对于边没有权重的情况下,是可以使用BFSDFS来求单源最短路径的!

2. Dijkstra算法

概述

  • 解决 单源最短路径 问题:求一个顶点到图中所有顶点的最短路径;
  • 保研机试经常考滴哦

基本思想

我觉得《算法笔记》上讲的太好了,哭了,那个数码宝贝,👴的最爱呀。下面的概念有点 (非常)苦涩.

设置 集合S 存放已被访问的顶点,然后执行n次下面的两个步骤(n为顶点个数):

  1. 每次从集合V-S(即未被访问的顶点)中选择与起点s的最短距离最小的一个顶点(记为u),访问并加入集合S(即代表已经被访问);
  2. 之后,令顶点u为中介点,优化起点s与所有从u能到达的顶点v之间的最短距离。

具体实现

Dijkstra算法的具体实现:

由于Dijkstra算法的策略比较偏重于理论化,因此为了方便编码,需要想办法来实现策略中较为关键的东西,即集合S的实现、起点s到达顶点 V i ( 0 ≤ i ≤ n − 1 ) V_i(0≤i≤n-1) Vi(0in1)的最短距离的实现。

  1. 集合S可以使用一个bool型数组vis[]来实现,即当vis[i] == true时表示顶点 V i V_i Vi已被访问,当vis[i] == false时表示顶点 V i V_i Vi未被访问。
  2. 令int型数组d[]表示起点s到达顶点 V i V_i Vi的最短距离,初始时除了起点sd[s]赋值为0,其余顶点都赋值为一个很大的数(初学者可以使用1000000000,即 1 0 9 10^9 109;懂二进制的童鞋使用0x3fffffff)来表示INF,即不可达。注意:不要写成0x7fffffff,这样做加法会溢出的。

伪代码

接下来看看实现Dijkstra算法的伪代码。

//G为图,一般设置为全局变量;数组d为源点到达各点的最短路径长度,s为起点
Dijkstra(G,d[],s){
	初始化;
	for(循环n次){	//每一次都会找到s到达一个顶点的最短路径
	 	u = 使d[u]最小的还未被访问的顶点的标号;
		记u已被访问;
		for(从u出发能到达的所有顶点v){
			if(v未被访问&&以u为中介点能使s到顶点v的最短距离d[v]最优){
				优化d[v]; 
			} 
		}	
	} 
} 

具体实现代码

常量定义

const int MAXV = 1000;	//设置最大顶点数
const int INF = 1000000000;	//设置INF为一个很大的数

邻接矩阵版

//邻接矩阵版
//适用于点数不大(例如V不超过1000)的情况,相对好写。代码如下:
int n,G[MAXV][MAXV];	//n为顶点数,MAXV为最大顶点数
int d[MAXV];	//起点到达各点的最短路径长度
bool vis[MAXV] = {false};	//标记数组,vis[i] == true表示已访问。初值均为false

void Dijkstra(int s){	//s为起点 
	fill(d,d+MAXV,INF);	//fill函数将整个d数组赋值为INF(慎用memset)
	d[s] = 0;	//起点s到达自身的距离为0
	
	for(int i=0;i<n;i++){	//循环n次 
	
		int u = -1,MIN = INF;	//u使d[u]最小,MIN存放该最小的d[u]
	
		for(int j=0;j<n;j++){	//找到未访问的顶点中d[]最小的 
			if(vis[j] == false && d[j] < MIN){
				u = j;
				MIN = d[j];
			}
		}
				
		//找不到小于INF的d[u],说明剩下的顶点和起点s都不相通
		if(u == -1){
			return;
		} 
		vis[u] = true;	//标记u为已访问
		for(int v=0;v<n;v++){
			//如果v未访问 && u能到达v && 以u为中介点可以使d[v]更优
			if(vis[u] == false && G[u][v] != INF && d[u] + G[u][v] < d[v]){
				d[v] = d[u] + G[u][v];	//优化d[v] 
			} 
		} 		 
	}
} 

从复杂度来看,主要是外层循环 O ( V ) O(V) O(V)(V就是顶点个数n)与内层循环(寻找最小的d[u]需要 O ( V ) O(V) O(V)、枚举v需要 O ( V ) O(V) O(V)产生),总复杂度为 O ( V ∗ ( V + V ) ) = O ( V 2 ) O(V * (V + V)) = O(V^2) O(V(V+V))=O(V2).

邻接表版

//邻接表版
struct Node{
	int v,dis;	//v为边的目标顶点,dis为边权 
}; 

vector<Node> Adj[MAXV];	//图G,Adj[u]存放从顶点u出发可以到达的所有顶点
int n;	//n为顶点数,图G使用邻接表实现,MAXV为最大顶点数
int d[MAXV];	//起点到达各点的最短路径长度
bool vis[MAXV] = {false};	//标记数组,vis[i]==true表示已访问,初值均为false

void Dijkstra(int s){	//s为起点 
	fill(d,d+MAXV,INF);		//fill函数将整个d数组赋值为INF(慎用memset) 
	d[s] = 0;	//起点s到达自身的距离为0
	for(int i=0;i<n;i++){
		int u=-1,MIN=INF;	//u使d[u]最小,MIN存放该最小的d[u]
		for(int j=0;j<n;j++){	//找到未访问的顶点中d[]最小的
		 	if(vis[j] == false && d[j] < MIN){
		 		u = j;
		 		MIN = d[j];
			 }	
		}
		
		//找不到小于INF的d[u],说明剩下的顶点和起点s不连通
		if(u == -1) return;
		vis[u] = true;	//标记u为已访问
		//只有下面这个for与邻接矩阵的写法不同
		for(int j=0;j<Adj[u].size();j++){
			int v = Adj[u][j].v;	//通过邻接表直接获得u能够到达的顶点v
			if(vis[v] == false && d[u] + Adj[u][j].dis < d[v]){
				//如果v未访问 && 以u为中介点可以使d[v]更优
				d[v] = d[u] + Adj[u][j].dis;	//优化d[v] 
			} 
		} 
	} 
} 

从复杂度来看,主要是外层循环O(V)与内层循环(寻找最小的d[u]需要 O ( V ) O(V) O(V)、枚举v需要O ( a d j [ u ] . s i z e ) (adj[u].size) (adj[u].size))产生的。又由于对整个程序来说,枚举v的次数总共为O( ∑ u = 0 n − 1 a d j [ u ] . s i z e = O ( E ) \sum_{u=0}^{n-1}adj[u].size=O(E) u=0n1adj[u].size=O(E)),因此复杂度为 O ( V 2 + E ) O(V^{2}+E) O(V2+E)

改进

上述的两种方法的复杂度都达到了 O ( V 2 ) O(V^2) O(V2) 级别,但是可以通过堆优化来降低寻找最小d[u]的过程,使其时间复杂度小于O(V)。

最简单的写法就是直接使用STL中的优先队列 priority_queue,这样使用邻接表实现的Dijkstra算法的时间复杂度可以降为 O ( V l o g V + E ) O(VlogV + E) O(VlogV+E)

此外, Dijkstra算法只能应对所有边权都是非负数的情况,如果边权出现负数,那么Dijkstra算法会出错,这时最好使用SPFA算法

  1. 如果是无向图的怎么办呢?
    直接把无向改成两个方向上的有向即可。
  2. 如何存储最短路径(而非仅仅是最短路径长度)?
    在优化d[u]中使用一个pre[u]数组来存储当前节点的前驱节点。然后使用DFS得到最优的路径。
  3. 如果最优路径不仅仅只有一条呢?
    那么一般会出现三种情况来让你对不止一条最优路径进行判断:
    新增边权
    新增点权
    求最短路径条数
    以上三步也都是在优化 d[u] 中进行操作。

3. 题型训练

  1. 最短路径——浙江大学
  2. 上交有一道题目不会做。。。

4. 参考文档

算法笔记

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值