一、最短路径(一般为有向图概念)
1、若是带有权值的图,最短路径就是两点之间权值和最小的路径。
2、若是不带权值的图,最短路径就是连通两点最少的边个数。
我们这里讨论带权有向图的情况。
二、最短路径算法
1、单源最短路径
定义
求图中某一个顶点到其它顶点的最短路径。
Dijkstra算法
基本原理
在所有未走过的顶点中选择权值和最小的顶点作为起点,去算终点的最小权值和(终点的原始权值和要与起点权值和加上路径权值比较,小的那一个就作为终点的最小权值和)。
此时我们需要三个辅助数组:
dist 来记录对应下标顶点的权值和。
visited 来记录已经走过的顶点。
pPath 来记录以该顶点为终点,最短路径的起点下标,一般称为父路径下标。
注意:此算法不能用于计算有负权的路径,因为此算法是基于多走一条路必然会增加权值的单调性的规律来做到局部贪心也能正确。下面介绍的Bellman-Ford算法就是来解决带有负权路径的情况。
举例
我们分别记s,y,z,t,x的下标是0,1,2,3,4。
dist初始化为无穷,pPath初始化为-1,visited初始化为false(起点是true)。
代码实现
//最短路径算法一
void Dijkstra(const V& src, vector<W>& dist, vector<int>& pPath)
{
int srci = GetIndex(src);
int n = _vertexs.size();
vector<bool> visited(n, false);
dist.resize(n, MAX_W);
pPath.resize(n, -1);
//初始化起点
dist[srci] = W();
pPath[srci] = srci;
visited[srci] = true;
for (int i = 0; i < n; i++)
{
W min = MAX_W;
int u = 0;
//for循环在未走过的顶点中选出最小权值的顶点
for (int j = 0; j < n; j++)
{
if (visited[j] == false && min > dist[j])
{
min = dist[j];
u = j;
}
}
//此时我们把u作为起点去更新终点的最小权值和 u-->k
for (int k = 0; k < n; k++)
{
if (_matrix[u][k] != MAX_W && dist[k] > dist[u] = _matrix[u][k])
{
dist[k] > dist[u] = _matrix[u][k];
pPath[k] = u;
}
}
//最后不要忘了把u变成走过的顶点
visited[u] = true;
}
}
Bellman-Ford算法
基本原理
可以用两层for循环遍历起点和终点,暴力选出最短路径。
注意三个问题
1、更新滞后
因为我们的for循环始终是起点从下标0更新到下标n-1,终点也是如此。导致了更新有先后顺序,先更新的路径可能会带有后更新最短路径的边,导致先更新的路径中保留了之前旧的边的权值,导致权值和变大,结果不准确。
解决办法是两层for循环后再套一层for循环,强制更新旧路径。
2、update优化
由于三层for循环效率太低,我们找到规律:若两层for循环中不再有权值更新,那结果就已经固定,所以可以加上bool值update来判断是否还要再更新下去(和冒泡排序优化相似)。
3、检查是否有负权回路
因为带负权回路,每一次更新都会比之前小,找不到最短路径,所以我们要检查是否有最短路径。若上述流程已经走完那必然不会再更新处最短路径,所以可以再函数末尾再来两层for循环,若有一次更新出最短路径就说明有负权回路。
代码实现
//最短路径算法二
bool Bellman_Ford(const V& src, vector<W>& dist, vector<int>& pPath)
{
int srci = GetIndex(src);
int n = _vertexs.size();
dist.resize(n, MAX_W);
pPath.resize(n, -1);
dist[srci] = W();
//由于会出现更新滞后 所以还要更新n次
for (int k = 0; k < n; k++)
{
//优化
bool update = false;
//两层for循环遍历 更新所有顶点到任意一个顶点的最短路径
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
//i-->j
if (_matrix[i][j] != MAX_W && dist[j] > dist[i] + _matrix[i][j])
{
update = true;
dist[j] = dist[i] + _matrix[i][j];
pPatn[j] = i;
}
}
}
if (update == false)
break;
}
//由于带有负权回路会永远更新下去,所以经过上述操作后理应不会再进行更新
//我们利用这一特性可以检查是否带有负权回路
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
if (_matrix[i][j] != MAX_W && dist[j] > dist[i] + _matrix[i][j])
return false;
}
}
return true;
}
2、多源最短路径
定义
求每对顶点间的最短路径。
Floyd-Warshall算法
基本原理
我们用到动态规划思想,设想在i->j的最短路径上可能存在中间顶点k,则设D(i j k)是从i到j只以(1,k)顶点集合中节点为中间节点的最短路径的长度。
则可推出如下公式:
经过节点k:D(i j k) = D(i k k-1) + D(k j k-1)
不经过节点k:D(i j k) = D(i j k-1)
则D(i j k) = min(D(i j k-1) + D(k j k-1), D(i j k-1))
代码实现
//最短路径算法三
void Floyd_Warhall(vector<vector<W>>& vvDist, vector<vector<int>> vvpPath)
{
int n = _vertexs.size();
//开空间
vvDist.resize(n);
vvpPath.resize(n);
for (int i = 0; i < n; i++)
{
vvDist[i].resize(n, MAX_W);
vvpPath[i].resize(n, -1);
}
//初始化
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
if (_matrix[i][j] != MAX_W)
{
vvDist[i][j] = _matrix[i][j];
//直接相连的父路径就是起点
vvpPath[i][j] = i;
}
if (i == j)
{
vvDist[i][j] = W();
}
}
}
//逻辑
for (int k = 0; k < n; k++)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
if (_matrix[i][k] != MAX_W && _matrix[k][j] != MAX_W
&& vvDist[i][j] > _matrix[i][k] + _matrix[k][j])
{
vvDist[i][j] = _matrix[i][k] + _matrix[k][j];
// 如果k->j 直接相连,上一个点就k,vvpPath[k][j]存就是k
// 如果k->j 没有直接相连,k->...->x->j,vvpPath[k][j]存就是x
vvpPath[i][j] = vvpPath[k][j];
}
}
}
}
}