Dijkstra算法

Dijkstra算法

Dijkstra算法:用来解决单源最短路问题。给定图G和起点s,通过算法得到S到达其他每个顶点的最短距离。

基本思想:对图G(V, E)设置集合S,存放已被访问的顶点,然后每次从集合V-S中选择与起点s的最短距离最小的一个顶点(记为u),访问并加入集合S。之后,令顶点u为中介点,优化起点s与所有从u能到达的顶点v之间的最短距离。这样的操作执行n次(n为顶点个数),直到集合S已包含所有顶点。

有向非负带权如图所示:
在这里插入图片描述
为了更加方便理解dijkstra算法,我们先把所有的有向边全部删掉,从源点开始,红色表示未被访问的点,白色表示已经被访问过的点,设v0到v0的路径为0,v0到所有点的距离为无穷大

如下图所示:从v0开始,我们将与v0相连的边全找出来,如下图所示,找到所有边中权值最小的边,并且,将与该条边相连的点标记为已经访问,此时,该条边就是v0到v1的最短路径。(可证明,若改条边不是v0到v1的最短路径,那么一定会有一条路径使得v0到v1经过中介点m,使得v0到v1路径最短,那么v0到v1的直接距离一定会比v0到中介点的距离大,这与我们找到的v0到v1是与v0相连路径中最短路径冲突)
在这里插入图片描述
此时,我们从最新标记已经访问过的点v1出发,找出所有与v1相连的,且以v1为出发点的有向边,并且找到所有有向边中的最短路径,此时我们发现,由v0到v3的直接路径为4,而v0经过v1为中介点到v3的距离为3,此时我们更新v0到v3的最短路径
在这里插入图片描述
此时,我们发现未被访问过的点,中v0到v3的距离是最短的,将v3更新为已经访问,此时,我们将以v3为出发点,与v3相连的有向边全部找出
在这里插入图片描述
此时,我们发现由v0直接到v4的路径比v0经过v3到v4的路径要短,因此,我们无需更新v0到v4的最短路径

v0经过v3到v2的距离由无穷大变为5,更新v0到v2的最短路径
在这里插入图片描述
我们发现由由v0出发,且与v0距离最小的点是v4,把v4设为已经访问,且找出以v4为出发点,与v4相连的有向边,我们发现v0到v5经过v4所需的路径为7,此时更新v0到v5的最短距离
在这里插入图片描述
由v0出发到未被访问的点且路径最短,此时找到v2,将该点变为已经访问,且找到以v2为出发点的有向边,此时更新v5的距离
在这里插入图片描述
找到未被访问的点,使得v0到该点的距离是所有未被访问点中路径最短的,找到v5,标记v5为已经访问
在这里插入图片描述
此时已经全部更新完成。

Dijkstra算法的策略:
设置集合S存放已被访问的顶点,然后执行n次下面的两个步骤(n为顶点个数)
1)每次从集合V-S中选择与起点s的最短距离最小的一个顶点(记为u),访问并加入集合S。
2)之后,令顶点u为中介点,优化起点s与所有从u能到达的顶点v之间的最短距离。

Dijkstra算法的具体实现:
1)集合S可以用一个bool型数组vis[]来实现,即当vis[i] == true时表示顶点Vi已被访问,当vis[i] == false时表示顶点Vi未被访问。
2)令int型数组d[]表示起点s到达顶点Vi的最短距离,初始时除了起点s的d[s]赋为0,其余顶点都赋为一个很大的数来表示inf,即不可达。

Dijkstra算法的伪代码

Dijkstra(G, d[], s)
{
	初始化;
	for(循环n次)
	{
		u = 使d[u]最小的还未访问的顶点的标号;
		记u已被访问;
		for(从u出发能到达的所有顶点v)
		{
			if(v未被访问 && 以u为中介点使s到顶点v的最短距离d[v]更优)
			{
				优化d[v];
			}
		}
	}
}

定义MAXV为最大顶点数、INF为一个很大的数字

const int MAXV = 1000; //最大顶点数
const int INF = 1000000000; //设INF为一个很大的数
  1. 邻接矩阵版
    需要枚举所有顶点来查看v是否可由u到达
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
	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[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v])
			{
				d[v] = d[u] + G[u][v]; //优化d[v]
			}
		}
	}
}
  1. 邻接表版
    直接得到u能到达的顶点v
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
	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与邻接矩阵的写法不同
		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]
			}
		}
	}
}

例题

从起点V0到达其他所有顶点都必须是最短距离,即将上述过程实现
在这里插入图片描述

#include <cstdio>
#include <algorithm>
using namespace std;
const int MAXV =1000; //最大顶点数
const int INF = 100000000; //设INF为一个很大的数

int n, m, s, G[MAXV][MAXV]; //n为顶点数,m为边数,s为起点
int d[MAXV]; //起点到达各点的最短路径长度
bool vis[MAXV] = {false}; //标记数组,vis[i]==true表示已访问。初值均为false

void Dijkstra(int s) //s为起点
{
	fill(d, d + MAXV, INF); //fill函数将整个d数组赋为INF
	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[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v])
			{
				d[v] = d[u] + G[u][v]; //优化d[v]
			}
		}
	}
}

int main()
{
	int u, v, w;
	scanf("%d%d%d", &n, &m, &s); //顶点个数、边数、起点编号
	fill(G[0], G[0] + MAXV * MAXV, INF); //初始化图G
	for(int i = 0; i < m; i++)
	{
		scanf("%d%d%d", &u, &v, &w); //输入u,v以及u->v的边权
		G[u][v] = w;
	}
	Dijkstra(s); //Dijkstra算法入口
	for(int i = 0; i < n; i++)
	{
		printf("%d ", d[i]); //输出所有顶点的最短距离
	}
	return 0;
}
//输入数据
6 8 0 //6个顶点,8条边,起点为0号。以下8行为8条边
0 1 1 //边0->1的边权为1,下同
0 3 4
0 4 4
1 3 2
2 5 1
3 2 2
3 4 3
4 5 3
0 1 5 3 4 6 //输出结果

题目若是无向边,则把无向边当成两条指向相反的有向边。

最短路径的求法
Dijkstra算法伪代码

if(v未访问 && 以u为中介点可以使起点s到顶点v的最短距离d[v]更优)
{
	优化d[v];
}

以u为中介点可以使起点s到顶点v的最短距离d[v]更优:使d[v]变得更小的方案是让u作为从s到v最短路径上v的前一个结点(s->…->u->v)。

启发:把这个信息记录下来,设置数组pre[],令pre[v]表示从起点s到顶点v的最短路径上v的前一个顶点(前驱结点)的编号。当伪代码中的条件成立时,就可以将u赋给pre[v],最终就能把最短路径上每一个顶点的前驱结点记录下来。

if(v未访问 && 以u为中介点可以使起点s到顶点v的最短距离d[v]更优)
{
	优化d[v];
	令v的前驱为u;
}

邻接矩阵

int n, G[MAXV][MAXV]; //n为顶点数,MAXV为最大顶点数
int d[MAXV]; //起点到达各点的最短路径长度
int pre[MAXV]; //pre[v]表示从起点到达顶点v的最短路径上v的前一个顶点
bool vis[MAXV] = {false}; //标记数组,vis[i]==true表示访问。初值均为false

void Dijkstra(int s) //s为起点
{
	fill(d, d + MAXV, INF); //fill函数将整个d数组赋为INF
	for(int i = 0; i < n; i++) 
	{
		pre[i] = i; //初始状态设每个点的前驱为自身
	}
	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[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v])
			{
				d[v] = d[u] + G[u][v]; //优化d[v]
				pre[v] = u; //记录v的前驱顶点是u
			}
		}
	}
}

从算法中已经可以得到每个顶点的前驱
在这里插入图片描述

pre[4] = 3;
pre[3] = 2;
pre[2] = 1;
pre[1] = 1;

当想要知道从起点V1到达V4的最短路径,就需要先从pre[4]得到V4的前驱顶点是V3,然后从pre[3]得到V3的前驱顶点是V2,再从pre[2]得到V2的前驱结点是V1

递归:用pre[]的信息寻找前驱,直至到达起点V1后从递归深处开始输出。

void DFS(int s, int v) //s为起点编号,v为当前访问的顶点编号(从终点开始递归)
{
	if(v == s) //如果当前已经到达起点s,则输出起点并返回
	{
		printf("%d\n", s);
		return;
	}
	DFS(s, pre[v]); //递归访问v的前驱顶点pre[v]
	printf("%d\n", v); //从最深处return回来之后,输出每一层的顶点号
}

碰到有两种及以上可以达到最短距离的路径,题目就会给出一个第二标尺(第一标尺是距离),要求在所有最短路径中选择第二标尺最优的一条路径。第二标尺常见的是一下 3 种出题方法或其组合,及解决其对应的解决方案:

只需要增加一个数组来存放新增的边权或点权或最短路径条数,然后在Dijkstra算法中修改**优化d[v]**的那个步骤即可,其他不改动。

  1. 新增边权:给每条边再增加一个边权(比如花费),然后要求在最短路径有多条时要求路径上的花费之和最小(如果边权是其他含义,也可以是最大)。
    解决:以花费为例,用cost[u][v]表示u->v的花费,并增加一个数组c[],令从起点s到达顶点u的最少花费为c[u],初始化时只有c[s]为0、其余c[u]均为INF。这样就可以在d[u] + G[u][v] < dv时更新d[v]和c[v],而当d[u] + G[u][v] == dv且c[u]+cost[u][v] < cv时更新c[v]。
for(int v = 0; v < n; v++)
{
	//如果v未访问 && u能到达v
	if(vis[v] == false && G[u][v] != INF)
	{
		if(d[u] + G[u][v] < d[v]) //以u为中介点可以使d[v]更优
		{
			d[v] = d[u] + G[u][v];
			c[v] = c[u] + cost[u][v];
		}
		else if(d[u] + G[u][v] == d[v] && c[u] + cost[u][v] < c[u])
		{
			c[v] = c[u] + cost[u][v]; //最短距离相同时看能否使c[v]更优
		}
	}
}
  1. 新增点权:给每个点增加一个点权(例如每个城市能收集到的物资),然后在最短路径有多条时要求路径上的点权之和最大(如果点权是其他含义的话也可以是最小)
    解决:用weight[u]表示城市u中的物资数目,并增加一个数组w[],令从起点s到达顶点u可以收集到的最大物资为w[u],初始化时只有w[s]为weight[s]、其余w[u]均为0。这样就可以在d[u]+G[u][v]<dv时更新d[v]和c[v],而当d[u]+G[u][v]==dv且w[u]+weight[v]>wv时更新w[v]。
for(int v = 0; v < n; v++)
{
	//如果v未访问 && u能到达v
	if(vis[v] == false && G[u][v] != INF)
	{
		if(d[u] + G[u][v] < d[v]) //以u为中介点可以使d[v]更优
		{
			d[v] = d[u] + G[u][v];
			w[v] = w[u] + weight[v];
		}
		else if(d[u] + G[u][v] == d[v] && w[u] + weight[v] > w[v])
		{
			w[v] = w[u] + weight[v]; //最短距离相同时看能否使w[v]更优
		}
	}
}
  1. 求最短路径条数:直接问有多少条路径
    解决:只需要增加一个数组num[]。令从起点s到达顶点u的最短路径条数为num[u],初始化时只有num[s]为1、其余num[u]均为0。这样就可以在d[u]+G[u][v]<dv时更新d[v],并让num[v]继承num[u],而当d[u]+G[u][v]==dv时将num[u]加到num[v]上。
for(int v = 0; v < n; v++)
{
	//如果v未访问 && u能到达v
	if(vis[v] == false && G[u][v] != INF)
	{
		if(d[u] + G[u][v] < d[v]) //以u为中介点可以使d[v]更优
		{
			d[v] = d[u] + G[u][v];
			num[v] = num[u];
		}
		else if(d[u] + G[u][v] == d[v])
		{
			num[v] += num[u]; //最短距离相同时累加num
		}
	}
}
  • 54
    点赞
  • 110
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阳光开朗男孩

你的鼓励是我最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值