dijkstra算法(单源最短路问题)(含邻接表 + 堆优化)

dijkstra算法是用于解决单源最短路问题的著名算法

问题的提出

给一个n=6个顶点的边带正权的有向图e,求起点1其余顶点的最短路径
在这里插入图片描述

准备工作

与Floyd算法一样,我们依然采用邻接矩阵表示法存放 e e e的信息:

在这里插入图片描述

除此之外,我们还需要用一个一维数组dis[N]来存储起点1到其余各个顶点的初始距离:

在这里插入图片描述
可以看见dis最初其实就是邻接矩阵e第一行的拷贝
接下来我们将所有顶点划分为两类:

  1. A类:顶点1和该类中的顶点之间的最短距离已求得;
  2. B类:A类以外的其他顶点;

顶点1到自身的距离就是最短距离,显然在最初,顶点1属于A类,其余属于B类。

算法步骤

  1. 在B类中寻找离顶点1最近的顶点,即确定一个i 使得dis[i]最小,将i加入A类。当前离顶点1最近的是顶点2,则此时dis[2]的值就是顶点1到顶点2的最短距离。为什么会有这一结论,我们来分析一下:

    "当前顶点 2离顶点 1最近,也就是说对任意 i!=2 都有 dis[i]>=dis[2]
    而且对任意的 i,j 都有 e[i][j]>0(即没有负权边),
    所以对任意的 k都有 dis[k]+e[k][2]>dis[2],换句话说,引入其他任意一个顶点作为中转点,顶点 1和顶点 2之间的距离都会变大,
    这就证明了此时 dis[2]的值就是顶点 1到顶点 2的最短距离 "

  2. 对于步骤1找到的i,对它的出边进行松弛操作。2->32->4这两条出边。我们先来看2->3,讨论通过这条边能否缩短顶点1到顶点3的距离,也就是判断
    dis[2]+e[2][3]<dis[3]是否成立,发现dis[2]+e[2][3]=1+9=10dis[3]=12,判断条件成立,则dis[3]的值更新为dis[2]+e[2][3]的值10,松弛成功。接下来对2->4进行同样的操作,将dis[4]更新为4
    对顶点2的所有出边进行松弛操作后,dis数组已经更新为:
    在这里插入图片描述

  3. 重复步骤1、2的操作,直到A类包含e中的所有顶点。 这里列出每次经过步骤2后dis数组的更新情况:

选取顶点4

在这里插入图片描述

选取顶点3

在这里插入图片描述

选取顶点5

在这里插入图片描述
在这个例子中顶点6没有出边,所以dis不作更新,最终的dis数组如下:
在这里插入图片描述
这便是顶点1到各顶点的最短路径。

献上代码:

#include <iostream>
#include <algorithm>
#include <queue>
#include <stack>
#include <stdio.h>
#include <limits.h>
#include <cmath>
#include <stdlib.h>
#include <string>
#include <vector>
#include <set>
#include <map>
#include <utility>
#include <windows.h>
#include <climits>

#define LL long long
#define INF 999999
using namespace std;

const double pi = atan(1.) * 4.;
const int N = 51;
const int MOD = 1e9 + 7;

int n, m;
int a, b, c;

int e[N][N];	//邻接矩阵
int dis[N];		//顶点1到顶点i的距离
int	book[N];	//记录顶点i是否属于A类
int dmin;		//和顶点1距离的最小值
int imin;		//和顶点1距离最近的顶点号	

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);

	//输入顶点数、边数
	cin >> n >> m;

	//准备工作-----------------------------------
	//构建邻接矩阵
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= n; j++)
			e[i][j] = (i == j) ? 0 : INF;

	//输入每条边的信息
	for (int i = 1; i <= m; i++) {
		cin >> a >> b >> c;
		e[a][b] = c;
	}

	//dis最初其实就是邻接矩阵e第一行的拷贝
	for (int i = 1; i <= n; i++) {
		dis[i] = e[1][i];
	}

	//将顶点1归到A类
	book[1] = 1;

	//核心步骤-----------------------------------
	//B类有n-1个点,循环n-1次
	for (int k = 1; k <= n - 1; k++){

		//遍历数组dis,寻找i使得dis[i]最小
		dmin = INF;
		for (int i = 2; i <= n; i++){
			if (book[i] == 0 && dis[i] < dmin) {
				dmin = dis[i];
				imin = i;
			}
		}

		//将寻找到的最小i归到A类
		book[imin] = 1;

		//对imin的所有出边进行松弛操作
		for (int j = 1; j <= n; j++) {
				dis[j] = min(dis[imin] + e[imin][j], dis[j]);
		}
	}

	//输出最终的dis数组
	for(int i = 1; i <= n; i++) 
		cout << dis[i] << ' ';
}
/*
input:
6 9
1 2 1
1 3 12
2 3 9
2 4 3
3 5 5
4 3 4
4 5 13
4 6 15
5 6 4

output:
0 1 8 4 13 17
*/

优化

实际上,上面描述的只是dijkstra算法的一个最基础的版本,它的复杂度是 O ( n 2 ) O(n^2) O(n2),在大数量级的顶点数和边数下效率并不高,我们可以从以下两方面着手,进行对算法的优化:

  1. 对于边数很少的稀疏图( m &lt; n 2 m&lt;n^2 m<n2)来说,邻接矩阵的存储效率很低,就像大盘子装小菜一样,会浪费很多无用的空间,在搜索的时候也会因为遍历太多“不存在的边”而效率下降;而用邻接表表示法只存储有效边,每次搜索的时候,有多少边就检查多少边,效率大大提高;
  2. 每次寻找离顶点1最近的顶点的时间复杂度为 O ( n ) O(n) O(n),可以用二叉堆来优化这一查找过程,每次获取距离最小的顶点,只需要直接拿出堆顶的数据即可。

优化后算法的复杂度可以降至 O ( n l o g n ) O(nlogn) O(nlogn)

优化后的代码:

#include <iostream>
#include <algorithm>
#include <queue>
#include <stack>
#include <stdio.h>
#include <limits.h>
#include <cmath>
#include <stdlib.h>
#include <string>
#include <vector>
#include <set>
#include <map>
#include <utility>
#include <windows.h>
#include <climits>

#define LL long long
#define INF 999999

using namespace std;

const double pi = atan(1.) * 4.;
const int N = 51;
const int MOD = 1e9 + 7;

int n, m;
int a, b, c;


struct node { //邻接表的结点结构
	int	v;	//终端顶点
	int w;	//边权
	node(int vv, int ww) :v(vv), w(ww) {}
	bool operator>(const node& other) const {
		return w > other.w;
	}
};

vector<node> e[N];	//邻接表
int dis[N];			//顶点1到顶点i的距离
int	book[N];		//记录顶点i是否属于A类
priority_queue<node, vector<node>, greater<node> > pq; //由于每次获取最小值,所以使用小顶堆
int imin;			//和顶点1距离最近的顶点号	

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);

	//输入顶点数、边数
	cin >> n >> m;

	//准备工作-----------------------------------
	for (int i = 1; i <= m; i++) {
		cin >> a >> b >> c;
		e[a].push_back(node(b, c));
	}

	memset(dis, INF, sizeof(dis));
	dis[1] = 0;

	for (int i = 0; i < e[1].size(); i++) {
		dis[e[1][i].v] = e[1][i].w;
		pq.push(e[1][i]);
	}

	book[1] = 1; //将顶点1归到A类
	
	//核心步骤-----------------------------------
	while (!pq.empty()) {
		imin = pq.top().v; //获取和顶点1距离最近的顶点号(堆顶元素)
		pq.pop();
		if (book[imin] == 1) //若已属于A类则舍弃它
			continue;
		//将imin归到A类
		book[imin] = 1;
		for (int i = 0; i < e[imin].size(); i++) {
			int tv = e[imin][i].v;
			int tw = e[imin][i].w;
			if (book[tv] == 0 && dis[tv] > dis[imin] + tw) {
				dis[tv] = dis[imin] + tw;
				pq.push(node(tv, dis[tv]));
			}
		}
	}

	for (int i = 1; i <= n; i++) cout << dis[i] << ' ';
}
/*
input:
6 9
1 2 1
1 3 12
2 3 9
2 4 3
3 5 5
4 3 4
4 5 13
4 6 15
5 6 4

output:
0 1 8 4 13 17
*/

如果我的总结有不足之处,十分欢迎读者指出,希望借此机会互相学习进步!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值