【算法百题之四十五】5分钟看懂SPFA(贝尔曼夫德算法优化版)算法
大家好,我是Lampard~~
很高兴又能和大家见面了,接下来准备系列更新的是算法题,一日一练,早日升仙!
今天的探讨的问题是:学习SPFA单源最短路径(动态逼近法)
若对SPFA,或者最短单源路径算法没有概念的同学可以先看看以上的两篇博客。读完之后能够对该算法有一个大致的概念。今天主要和大家一起通过代码一步步实现该算法。今天用于测试的示例图:
(1)SPFA算法的主要构成
在我看来SPFA算法主要有以下几部分构成:
- 地图块结点
- dis距离向量
- path前驱向量
- queue算法实现队列
- isInqueue向量
(1)地图块结点
地图块结点有三个属性,包含了当前结点对其他节点的路径权值(用map实现),若不存在则设为无穷。num字段,记录被引用的次数(用于判断是否进入了死循环),index字段记录自己是第几个节点
(2)dis距离向量
存储起点与所有结点之间的距离(结果),初始化时除了起点之外其余都设为无穷大,起点距离则设为0
(3)path前驱向量
存储没一个结点的前驱结点(父亲结点),用于从结果回溯找到所有路径
(4)queue算法实现队列
用于实现动态逼近,当对列为空时代表算法结束(非死循环的状态下)
(5)isInqueue向量
记录某一个结点是否在队列中,其实我们也可以遍历队列查找,但是比较耗,现在我们就用一种空间换时间的方式来解决
(2)SPFA算法寻路过程
- 初始化各个地图块结点,设置相互之间的路径权值。
- 初始化dis距离向量,除起点外所有的距离设置为INT_MAX,起点本身设置为0
- 初始化path前驱向量,除起点外所有节点设置为-1(不可达),起点本身设置为自身的index
- 把起点放入队列中,然后进入循环。
- 循环体:把队列的元素取出(设为结点u),把U的num字段自增1,然后遍历其所有身边的结点(设为结点V)进行判断:dis【u】 + u到v的权值 < dis【v】?
- 若是(重点),则dis【v】赋值为dis【u】 + u到v的权值,然后把path【v】的前驱结点位置设置为u的位置,并把V结点放入队列中(若本身在队列中则不需要),进行下一结点判断
- 若否,则不作操作,进行下一结点判断,直至算法结束
- 算法结束标志有两个:1.队列为空,则证明已经找到起点距离各个位置的最短路径了。2.某一结点的num字段 > n(n是地图块的总数),证明其存在负权环进入了死循环
看到第8点可能有同学会存在疑惑,什么是负权环?为什么存在负权环就会进入死循环?
负权环:如果存在一个环(从某个点出发又回到自己的路径),而且这个环上所有权值之和是负数,那这就是一个负权环
我们留意算法的第6点,我们是通过松弛操作对各条路径进行更新,松弛操作的原理是著名的定理:“三角形两边之和大于第三边”,在信息学中我们叫它三角不等式。所谓对i,j进行松弛,就是判定是否d[j]>d[i]+w[i,j],如果该式成立则将d[j]减小到d[i]+w[i,j],否则不动。那么假设有一个三角形三条边都是负值,那么负数加一个负数肯定不会比第三条边大,那么此时第三条边又会进行更新把身边两个结点压入队列,然后重复这个过程进入死循环。所以我们这里有一个判断基准,若一个结点被引用超过n次(n为总结点数)则证明其进入了死循环。
过程演示图:
(3)原理就这么简单,上代码!!!
首先把有向图衣矩阵形式输出出来,然后进入我们的初始化过程
int spfaMap[5][5] = {
{INT_MAX, 2, INT_MAX, INT_MAX, 10},
{INT_MAX, INT_MAX, 3, INT_MAX, 7},
{INT_MAX, INT_MAX, INT_MAX, 4, INT_MAX},
{INT_MAX, INT_MAX, INT_MAX, INT_MAX, 5},
{INT_MAX, INT_MAX, 6, INT_MAX, INT_MAX},
};
(1)初始化
以下是我们定义好的地图块结点:index字段记录是第几个结点,num字段记录其被引用多少次,disMap记录其与其他节点的路径权值。紧接着初始化各个地图块结点,设置相互之间的路径权值。
struct spfaNode {
map<int, int> disMap;
int num = 0;
int index;
};
vector<spfaNode*> allNodes;
for (int i = 0; i < NUM; i++) {
spfaNode* node = new spfaNode;
node->index = i;
for (int j = 0; j < NUM; j++) {
if (spfaMap[i][j] != INT_MAX) {
node->disMap[j] = spfaMap[i][j];
}
}
allNodes.push_back(node);
}
然后进入算法的2,3步,初始化我们的dis向量和path和isInqueue向量。dis向量除了初始的index之外都设置成INT_MAX, path向量除初始位置之外都设置为-1(无前驱结点)
vector<int> disVector;
vector<int> pathVector;
vector<int> isInqueue;
for (int i = 0; i < NUM; i++) {
if (i == startIndex) {
disVector.push_back(0);
pathVector.push_back(i);
isInqueue.push_back(0);
continue;
}
disVector.push_back(INT_MAX);
pathVector.push_back(-1);
isInqueue.push_back(0);
}
(2)算法
初始化的步骤至此就已经完成了,紧接着我们就是进行SPFA的具体算法,首先第一步是把其实结点压入队列。然后我们对其实位置的isInqueue的值进行更新,代表其已经进入队列。
queue<spfaNode*> spfaQueue;
spfaQueue.push(allNodes[startIndex]);
isInqueue[startIndex] = 1;
紧接着构建循环体,进入算法5,6。首先我们需要把队列顶的元素给pop出来,然后将其isInqueue的值进行更新。在进入松弛操作之前我们需要进行一个判断,是否已经进入了死循环?若是则退出函数输出报错信息。若否则对u结点的num字段+1代表其被引用了一次。
while (!spfaQueue.empty()) {
spfaNode* uNode = spfaQueue.front();
spfaQueue.pop();
isInqueue[uNode->index] = 0;
if (uNode->num > NUM) {
cout << "存在负权环,程序进入了死循环" << endl;
return;
}
uNode->num = uNode->num + 1;
......
}
若没有报错,我们就进行我们的松弛操作。之后就遍历其所有的所有的结点,遍历周边所有的点如何实现?我们不是用一个map来存储其路径的权值吗,我们可以通过使用迭代器遍历map来获取其周围结点的index和路径权值。这里稍微讲解一下,对于map的迭代器来说,it->first是键值对的键,it->second是键值对的值。若对map容器感兴趣的可以看一下我这一片博客:【stl中的map和set容器】 。然后根据我们的算法第6步进行更新,若符合条件则进行信息的更新,以及把该节点压入队列。
while (!spfaQueue.empty()) {
spfaNode* uNode = spfaQueue.front();
spfaQueue.pop();
isInqueue[uNode->index] = 0;
if (uNode->num > NUM) {
cout << "存在负权环,程序进入了死循环" << endl;
return;
}
uNode->num = uNode->num + 1;
for (map<int, int>::iterator it = uNode->disMap.begin(); it != uNode->disMap.end(); it++) {
int targetIndex = it->first;
int targetRoad = it->second;
if (disVector[uNode->index] + targetRoad < disVector[targetIndex]) {
disVector[targetIndex] = disVector[uNode->index] + targetRoad;
pathVector[targetIndex] = uNode->index;
if (isInqueue[targetIndex] == 0) {
isInqueue[targetIndex] = 1;
spfaQueue.push(allNodes[targetIndex]);
}
}
}
}
至此我们整个算法流程已经实现。
(4)测试结果
答案验证是与下图相同。
代码都写了,要不我们测一测存在负权环的情况?
我把0结点到1结点的权值设置成了-2, 结点1到结点2的权值设置成-3, 结点2到结点0的权值设置成4 (此时结点0,1,2形成环,且-2 + -3 + 4 < 0, 所以是一个负权环),答案如我们所料会进入死循环中。