最短路径算法 Dijkstra和Prim详解

前言

这几次面试的确有好多次问到了最短路径算法,当时答得确实不好,对算法理解的不是很透彻,下面简单梳理一下。

Dijkstra算法

Dijkstra算法有点DP的意思,适用于单源最短路径算法且要求边的权值非负,同时可以用于有向图和无向图。

数据结构定义:

  • map[][]:存放原始图;
  • dis[]:存放从源点出发到达点i的最短路径长度;
  • visited[]:记录节点是否已被访问。
    初始化:
  • dis[i] = map[0][i],从源点出发到达各点的距离,无法到达记为INT_MAX;
  • visited(map.size(), 0), visited[0] = 1,初始只访问过源点。

思想:

双集合的说法可以查到很多,也解释的很详细,这里说一下我的理解。记源点为A。

  • 遍历dis数组(一共做n-1次),找到目前从源点出发,到达未访问节点i的最短路径;
  • 更新visited数组,置visited[i] = 1
  • 更新目前源点经由i中转到未访问节点的最短路径(假设i=B),如A->B->C,C为一未访问节点。这里的目前是指现在只能保证A->B的路径最短,更新得到的A->B->C路径只能保证现在是最短的。
    但是绝不可能有经过某一节点j中转到达i的路径比这一步找到的路径短,因为后面找到的可以从源点A到达的未访问点(假设目前找到了A->B,存在B<->C,):
    • 要么本来A可以访问到C;存在A->C,则有A->C->B,如果A->C->B的路径短于A->B,则必有A->C的路径短于A->B的路径,那么这一步找到的最短路径便是A->C,不会是A->B;
    • 要么本来A访问不到C;此时访问到C,只有A->B->C,假如有一条路径可以经过C和其他节点到达B,如A->B->C->D->B,因为到达C首先已经经过了B,在经由C中转到达B的路径必包含A->B->C,这种情况不可能发生。
  • 重复上述过程n-1次,即可找到从源点出发到达任意节点的最短路径。

参考代码

#include
#include
#include

using namespace std;

vector<int> Dijkstra(vector<vector<int>> &map);

int main(int argc, char *argv[]) {
	const int M = INT_MAX;
	vector<vector<int>> map
	{
		{0,6,3,M,M,M},
		{6,0,2,5,M,M},
		{3,2,0,3,4,M},
		{M,5,3,0,2,3},
		{M,M,4,2,0,5},
		{M,M,M,3,5,0}
	};
	auto res = Dijkstra(map);
	for (auto &ele : res)
		cout << ele << " ";
	cout << endl;
	return 0;
}

vector<int> Dijkstra(vector<vector<int>> &map)
{
	int size = map.size();
	vector<int> dis(size, INT_MAX), visited(size, 0);
	//初始化
	for (int i = 0; i < size; i++)
		dis[i] = map[0][i];
	visited[0] = 1;
	int t = size - 1;
	while (t--)
	{
		int minval = INT_MAX, next;
		//找到未访问节点中最小的路径,并记录该节点
		for (int i = 1; i < size; i++)
		{
			if (!visited[i] && dis[i] < minval)
			{
				minval = dis[i];
				next = i;
			}
		}
		//更新visited
		visited[next] = 1;
		for (int i = 0; i < size; i++)
		{
			//对于节点next无法到达的节点,或者已经访问得到最短路径的节点,跳过
			if (map[next][i] == INT_MAX || visited[i])
				continue;
			//检查中转路径是否比原路径短
			dis[i] = min(dis[i], minval + map[next][i]);
		}
	}
	return dis;
}

Prim算法

Prim算法适用于多源最短路径且不要求边的权值非负,同时可以用于有向图和无向图,但用于有向图的话要求两点之间来回的权值必须相同(待考证)。

数据结构定义

  • dis[][]代表从一点出发到达另一点的最短路径距离。

思想

从A到B的最短路径,要么有:

  • A>B,代表从A直接到B;
  • A->C->B,代表从A经过任意次中转到B。
    Prim算法的思想就是,既然有两种方法,遍历一遍所有可能,不断更新即可。

代码就三层循环,很暴力,有两点要注意:

  • 防止溢出检查;
  • 对k的循环要放在最外层,如果放在内层的话,会导致提前确定了两节点的最短路径,算法计算最短路径的前提是经过其他节点的中转路径也是最短的,显然这样做没有满足这一条件(类比于DP中的子问题未得到最优解);
    k的意思是:从i到j经过至多k个节点所得的最短路径,把k放在外层,先计算经过至多一个节点中转的最短路径,在计算经过2个。。。这样后面的子问题就可以使用前面子问题的最优解。
    这里还有一点,至多经过k个指的是按顺序经过第0,1,2,3,4…n-1个节点,每个节点可以经过也可以跳过。

参考代码

#include
#include
#include

using namespace std;

vector<vector<int>> Prim(vector<vector<int>> &map);

int main(int argc, char *argv[]) {
	const int M = INT_MAX;
	vector<vector<int>> map
	{
		{0,6,3,M,M,M},
		{6,0,2,5,M,M},
		{3,2,0,3,4,M},
		{M,5,3,0,2,3},
		{M,M,4,2,0,5},
		{M,M,M,3,5,0}
	};
	auto res1 = Prim(map);
	for (auto &a : res1)
	{
		for (auto &b : a)
			cout << b << " ";
		cout << endl;
	}
	return 0;
}

vector<vector<int>> Prim(vector<vector<int>> &map)
{
	vector<vector<int>> dis(map.begin(), map.end());
	int size = map.size();
	for (int k = 0; k < size; k++)
		for (int i = 0; i < size; i++)
			for (int j = 0; j < size; j++)
				//考虑溢出
				if (dis[i][k] != INT_MAX && dis[k][j] != INT_MAX && dis[i][j] > dis[i][k] + dis[k][j])
					dis[i][j] = dis[i][k] + dis[k][j];
	return dis;
}

参考文献

https://blog.csdn.net/ZHUO_SIR/article/details/80628663
https://blog.csdn.net/arthu6/article/details/80596772
https://www.cnblogs.com/hxsyl/p/3270401.html
https://cnblogs.com/ShiveryMoon/p/7859360.html

你可能感兴趣的:(c++与算法,算法,最短路径,毕节斯特拉)