Dijkstra实现单源最短路

发布时间 2023-12-28 23:00:51作者: 凪风sama

Dijkstra算法求单源最短路

Dijkstra算法应用于求一个给定图的单个源点到其他各顶点的最短路。其中应用Dijkstra算法的图应满足如下条件

  • 图中没有负权边
  • 有向或者无向图都可以
  • 图中若有自环或者重边也可以(需要自己先筛选一下)

Dijkstra算法的核心就是从源点开始对各个顶点进行松弛操作,且每一次松弛选择出当前最优也就是离源点最小的路径(贪心),直到求出总体最小的路径。其贪心的正确性证明可以看这个AcWing 849. Dijkstra算法(含正确性证明) - AcWing

朴素的Dijkstra算法

所谓朴素的Dijkstra就是在寻找当前未收入最短路的最小顶点时采用最朴素的遍历操作来寻找,与后面采用堆来比较朴素。
实现Dijkstra的步骤如下

  1. 初始化dist数组为正无穷,且令dist[s] = 0;s代表源点,dist[k]表示k顶点到源点的距离。源点本身到自身距离为0,其他顶点尚未更新默认为正无穷。
  2. 遍历所有顶点,从未收入最短路(st[k] == false)且更新过(dist[k] != 0x3f3f3f3f)的顶点中找到到源点距离最小的点,即为v。
  3. 将v收入到最短路中(st[v] = true),使用v更新v的邻接点到源点的距离,当v的邻接点到源点的距离大于v到源点再到该点的距离时,更新。

实现代码如下
849. Dijkstra求最短路 I - AcWing题库

#include <iostream>
#include <cstring>
using namespace std;
const int N = 600;
int g[N][N],dist[N];	//g[N][N],图的存储,dist[N],各顶点到源点的距离
bool st[N];				//顶点是否已经收入最短路
int n;					//顶点个数
void Dijkstra(int v)// v - > 源点
{
	memset(dist, 0x3f, sizeof(dist));
	dist[v] = 0;
	for (int i = 0; i < n; i++) //一共n个顶点,最多做n次操作即可更新所有顶点
	{
		int tmp = -1;
		for (int j = 1; j <= n; j++)
		{
			if (!st[j] && (tmp == -1 || dist[tmp] > dist[j]))
				tmp = j;
		}	//从未收入且更新过的顶点中选取最小值。由于未更新的节点为正无穷,不用特别判断也会自动被覆盖
		st[tmp] = true;	//将上述顶点收入
		for (int j = 1; j <= n; j++)
		{
			if (g[tmp][j] != -1)//若为tmp的邻接点
			{
				dist[j] = min(dist[j], dist[tmp] + g[tmp][j]);
			}
		}
	}
}

int main()
{
	memset(g, -1, sizeof(g));
	int m;
	cin >> n >> m;
	for (int i = 0; i < m; i++)
	{
		int x, y, z;
		cin >> x >> y >> z;
		g[x][y] = g[x][y] == -1 ? z : min(g[x][y], z);//若有重边,仅保留重边中较小的那条边
	}
	Dijkstra(1);
	cout << (dist[n] == 0x3f3f3f3f ? -1 : dist[n]);
	return 0;
}

几点细节

  1. 存储图的数组一般初始化为-1。
  2. 存储路径的数组初始化为无穷大,dist[源点] = 0;
  3. 若题目中说有重边,可以在建图时保留最小的一条边
堆优化的Dijkstra

可以看出朴素Dijkstra算法中寻找未收入的最小顶点这一步,每一次循环都要遍历一遍顶点,最坏要循环n次,也就是说这一步的时间复杂度为\(O(n^2)\)
不妨重新审视这个操作。我们要求的只是寻找某个最小值的操作,因此可以使用小根堆这一数据结构来优化这个操作。对于n个顶点,求其最小值的时间复杂度为\(O(1)\),更新堆中的顶点的复杂度为\(O(logn)\),这样优化后的总时间复杂度为$$O(nlogn)$$

直接上代码
注意这里使用邻接表建图且STL实现的数据结构居多
850. Dijkstra求最短路 II - AcWing题库

#include <iostream>
#include <queue>
#include <vector>
#include <cstring>
using namespace std;
const int N = 1e5 + 20;
typedef pair<int, int> PII;	//使用pair来存储顶点,first是该顶点的dist,second是该顶点的编号
vector<PII> g[N];
int dist[N];
bool st[N];
int n, m;
void Dijkstra(int v)
{
	memset(dist, -1, sizeof(dist));
	dist[v] = 0;
	priority_queue<PII, vector<PII>, greater<PII>>h;//建立小根堆的方法,优先队列默认大根堆
	h.push({ 0,v }); //将源点加入堆中。first为dist的目的是让其作为第一关键字进行排序
	while (!h.empty())	//结束条件为队列空,也就是所有顶点都出队了
	{
		auto tmp = h.top();//取出最小未收录的最小顶点
		h.pop();
		int seq = tmp.second, distance = tmp.first;
		if (st[seq]) continue;//若该顶点已经加入最短路,说明上述tmp是一个冗余元素,直接丢掉进入下一轮操作
		st[seq] = true; //标记为已经加入最短路
		for (int j = 0; j < g[seq].size(); j++)
		{
			int q = g[seq][j].first, p = g[seq][j].second;
			if (dist[q] > dist[seq] + p) // 若可以更新
			{
				dist[q] = dist[seq] + p; //更新
				h.push({ dist[q],q }); //加入更新队列中
			}
		}

	}
}
int main()
{
	cin >> n >> m;
	for (int i = 0; i < m; i++)
	{
		int x, y, z;
		cin >> x >> y >> z;
		g[x].push_back({y,z}); //编号,边权
	}
	Dijkstra(1);
	cout << (dist[n] == 0x3f3f3f3f ? -1 : dist[n]);
	return 0;
}