图的应用--最短路算法

本节适合对最短路稍有了解的读者阅读。最短路是图论这一节中重要的应用,涉及到了相当多的算法。当然这些算法可以不用全部掌握,但最少要略知一二。最短路问题求解主要有两个方向,一个是单源最短路,还有一个是多源最短路(就是是否只有一个起点)。单源最短路求解方法包含了Dijkstra算法,Bellman-ford算法和SPFA算法,而多源最短路问题主要就是用Floyd算法解决,但其时间复杂度较高,代码较为简单,一般算法竞赛中考的比较少(目前本蒟蒻是这样认为的)。

算法分类大概如下所示:

图的应用--最短路算法_第1张图片

首先是Dijkstra算法,这是所有人都应当熟练掌握的最短路算法。如果图的存储方式不同(邻接矩阵和邻接表),这一种算法的代码根据题目需要也会有稍许不同。

我们先看个最简单易学的代码,运用邻接矩阵存储(具体介绍都放注释里了)

#include
#include
#include
using namespace std;
const int N = 105;
int graph[N][N];   //用邻接矩阵存储图,注意:有时候算法题,是个稀疏图,点数多,而边数少,容易爆栈
bool st[N];  //表示该点是否已经确定好了最短路
int dist[N];  //表示起点到该点的最短路
int n, m;  //分别表示点数和边数

int dijkstra()
{
	memset(dist, 0x3f, sizeof dist);
	dist[1] = 0;  //初始化别忘了,之前笔者在这个地方debug了好久
	for (int i = 0;i < n;i++)
	{
		int t = -1;   //t初值为-1
		for (int j = 1;j <= n;j++)
		{
			if (!st[j] && (t == -1 || dist[t] > dist[j]))  //每次找到尚未确定最短路的,但目前距离起点最短的点
			{
				t = j;
			}
		}
		st[t] = true;  //将该点状态设为已确定
		for (int j = 1;j <= n;j++)
		{
			dist[j] = min(dist[j], graph[t][j] + dist[t]);  //用t去优化每一个点的最短路
		}
	}
	if (dist[n] == 0x3f3f3f3f) return -1;  //如果终点到起点的距离无穷大,说明没有路径可以从起点到终点,返回-1
	return dist[n];
}

int main()
{ 
	cin >> n >> m;
	memset(graph, 0x3f, sizeof graph);  //无穷大表示两点之间没有边
	while (m--)  //将每一条边存入graph中
	{
		int a, b, c;  //分别表示边的两端和权重
		cin >> a >> b >> c;
		graph[a][b] = min(graph[a][b],c);  //取最短的重边
	}
	cout << dijkstra();
	return 0;
}

认真看懂算法的读者,可能会想,开一个n2空间复杂度的数组,如果是稠密图还好,要是稀疏图的话不就太浪费空间了吗。甚至在算法竞赛中有的题目会故意卡空间,那不就谢了吗?所以我们可以换种方式--邻接表来存储图。用这种方式存储图,空间o(n)左右,但时间复杂度不会降低。邻接表可以用模拟数组的方式或者vector容器实现(STL大法)。

模拟数组实现邻接表的Dijkstra算法

//数组模拟
#include
#include
#include
using namespace std;
const int N = 105;
const int M = 10005;
int u[M], v[M], w[M]; //存储每一条边的两端和权重,**如果是无向图,记得数组开两倍
int Head[N]; 
int Next[N];
bool st[N];  //表示该点是否已经确定好了最短路
int dist[N];  //表示起点到该点的最短路
int n, m;  //分别表示点数和边数

int dijkstra()
{
	memset(dist, 0x3f, sizeof dist);
	dist[1] = 0;
	for (int i = 0;i < n;i++)
	{
		int t = -1;   //t初值为-1
		for (int j = 1;j <= n;j++)
		{
			if (!st[j] && (t == -1 || dist[t] > dist[j]))  //每次找到尚未确定最短路的,但目前距离起点最短的点
			{
				t = j;
			}
		}
		st[t] = true;  //将该点状态设为已确定
		int k = Head[t];
		while(k != -1)
		{
			dist[v[k]] = min(dist[v[k]], dist[t] + w[k]);
			k = Next[k];
		}
	}
	if (dist[n] == 0x3f3f3f3f) return -1;  //如果终点到起点的距离无穷大,说明没有路径可以从起点到终点,返回-1
	return dist[n];
}
int main()
{ 
	cin >> n >> m;
	for (int i = 1;i <= n;i++)	//初始化head数组下标1~n的值为-1,表示1~n顶点暂时都没有边
	{
		Head[i] = -1;
	}
	for (int i = 1;i <= m;i++)
	{
		cin >> u[i] >> v[i] >> w[i];
		Next[i] = Head[u[i]];  //next和head里存储的是第几条边,而不是顶点
		Head[u[i]] = i;
	}
	cout << dijkstra();
	return 0;
}

模拟数组实现邻接表的方式可能要多看几遍,确实不容易理解。当然看不懂也没事,我们可以用vector容器来实现。

vector实现邻接表的Dijkstra算法

​
//用邻接表存储的dijkstra算法
//邻接表存储一种方式使用vector容器,另一种方式是用数组模拟
#include
#include
#include
#include
using namespace std;
const int N = 105;

struct Point
{
	int u, w;
};
vectorg[N];  //用容器开辟一个邻接表
bool st[N];  //表示该点是否已经确定好了最短路
int dist[N];  //表示起点到该点的最短路
int n, m;  //分别表示点数和边数

int dijkstra()
{
	for (int i = 0;i < n;i++)
	{
		int t = -1;   //t初值为-1
		for (int j = 1;j <= n;j++)
		{
			if (!st[j] && (t == -1 || dist[t] > dist[j]))  //每次找到尚未确定最短路的,但目前距离起点最短的点
			{
				t = j;
			}
		}
		st[t] = true;  //将该点状态设为已确定
		for (int k = 0;k < g[t].size();k++)
		{
			dist[g[t][k].u] = min(dist[g[t][k].u], dist[t] + g[t][k].w);
		}
	}
	if (dist[n] == 0x3f3f3f3f) return -1;  //如果终点到起点的距离无穷大,说明没有路径可以从起点到终点,返回-1
	return dist[n];
}
int main()
{
	cin >> n >> m;
	while (m--)
	{
		int a, b, c;  //a,b,c分别表示边的两个端点和权重
		cin >> a >> b >> c;
		g[a].push_back({ b,c });
	}
	//遍历每一条边只要遍历整个容器就行
	cout << dijkstra();
	return 0;
}

​

 STL确实是个好东西,写出来的代码浅显易懂。(什么vector也看不懂or没学过?右上角点一下,谢谢。)

当一种算法学会了之后,我们会很自然的想这种算法能不能再次优化。对于稀疏图其实是可以的,我们用优先队列对其进行堆优化,每次取边只要取小根堆的顶端即可,保证取到的是最短距离。笔者在此不再赘述,对自己要求高的读者,可以自行搜索资料(才不是笔者懒得写呢)。优化后可以把复杂度降到o(mlogn)。

Dijkstra算法有一定的局限性,不能处理负权图。

所以便需要一种新的算法,Bellman-ford算法,这种算法可以处理负权图的问题。同时题目有时候还会加上限制条件,例如:最多只能走多少条边到达终点,这样用dijkstra更不行了。下面代码中的k指的就是最多能走多少条边。

Bellman-ford算法求解最短路

//Bellman-ford算法,可以处理含有负环的题目,但主要用于解决有路径条数限制的题目
#include
#include
#include
using namespace std;

const int N = 510;
const int M = 10010;
int n, m, k;
int dist[N];
int backup[N];   //用于限制边数后,防止修改一条边的长度,发生连锁反应,修改多条边的长度
struct Edge {     //Bellman-ford直接存储边
	int a, b, w;  
}edges[M];  //存边

int bellman_ford()
{
	for (int i = 0;i < k;i++)  //限制了k条路径
	{
		memcpy(backup, dist, sizeof dist);  //由于边数限制,修改边长时,可能会一连串修改,所以加入拷贝的数组
		for (int j = 0;j < m;j++)
		{
			int a = edges[j].a;
			int b = edges[j].b;
			int w = edges[j].w;
			dist[b] = min(dist[b], backup[a] + w);
		}
	}
	if (dist[n] > 0x3f3f3f3f / 2) return -1;  //表示不存在
	else return dist[n];
}

int main()
{
	cin >> n >> m >> k;
	for (int i = 0;i < m;i++)
	{
		int a, b, w;
		cin >> a >> b >> w;
		edges[i] = { a,b,w };
	}
	memset(dist, 0x3f, sizeof dist);
	dist[1] = 0;
	int t = bellman_ford();
	cout << t;
	return 0;
}

看完代码,可以发现这个算法只是把每条边存储了起来,还加上了一个拷贝数组,这是尤为需要注意的。那么Bellman-ford算法还能优化吗,答案是可以的,用队列优化(不是优先队列),代码有些类似于BFS。这种优化后的算法称为SPFA,时间复杂度严格意义上来说和Bellman-ford一致的,都是o(nm),但那只是最坏情况,除非出题人恶心,要不然时间复杂度会降低的。还要注意的是SPFA只能处理负权图,不能处理负权回路。同样的笔者只是提一下,具体感兴趣的读者自己查找资料学习。

单源最短路就讲这么多。

多源最短路就一个算法Floyd算法,本质上是一种动态规划的思想。其代码量和动态规划的特点一样----小,但是并不好理解,笔者不赘述。

//floyd算法,能用于求随便两个点之间的最短路,算法复杂度是n3
#include
#include
#include
using namespace std;
int n,m;
const int N = 105;
int d[N][N];
void floyd() 
{
	for (int k = 1; k <= n; k++)
	{
		for (int i=1;i<=n;i++)
		{
			for (int j=1;j<=n;j++)
			{
				d[i][j] = min(d[i][j], d[i][k] + d[k][j]);  //本质上是动态规划
			}
		}
	}
}

int main()
{
	cin >> n >> m;
	memset(d, 0x3f, sizeof d);
	while (m--)
	{
		int a, b, c;
		cin >> a >> b >> c;
		d[a][b] = c;
	}
	floyd();
	     //打印你想求的两个点就行了
	return 0;
}

可能有的读者会想,既然这个代码这么短,还能适用于各种情况,那么还要学上面的干嘛?原因很简单,因为时间复杂度太高,o(n3)。

大家这篇博客可以多花一些时间看,辅以别的资源,慢慢学习这些算法。笔者没有好好讲一些算法的优化,需要感兴趣的读者自行寻找资料了。end~~~

你可能感兴趣的:(数据结构,数据结构,图论,c语言,算法)