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