【算法百题之四十五】5分钟看懂SPFA算法(含代码)

【算法百题之四十五】5分钟看懂SPFA(贝尔曼夫德算法优化版)算法

 

     大家好,我是Lampard~~

     很高兴又能和大家见面了,接下来准备系列更新的是算法题,一日一练,早日升仙!

     今天的探讨的问题是:学习SPFA单源最短路径(动态逼近法)

   【spfa算法详解1】

   【spfa算法详解2】

   若对SPFA,或者最短单源路径算法没有概念的同学可以先看看以上的两篇博客。读完之后能够对该算法有一个大致的概念。今天主要和大家一起通过代码一步步实现该算法。今天用于测试的示例图:

  

(1)SPFA算法的主要构成

   在我看来SPFA算法主要有以下几部分构成:   

  • 地图块结点
  • dis距离向量
  • path前驱向量
  • queue算法实现队列
  • isInqueue向量

(1)地图块结点

  地图块结点有三个属性,包含了当前结点对其他节点的路径权值(用map实现),若不存在则设为无穷。num字段,记录被引用的次数(用于判断是否进入了死循环),index字段记录自己是第几个节点

(2)dis距离向量

  存储起点与所有结点之间的距离(结果),初始化时除了起点之外其余都设为无穷大,起点距离则设为0

(3)path前驱向量

  存储没一个结点的前驱结点(父亲结点),用于从结果回溯找到所有路径

(4)queue算法实现队列

  用于实现动态逼近,当对列为空时代表算法结束(非死循环的状态下)

(5)isInqueue向量

  记录某一个结点是否在队列中,其实我们也可以遍历队列查找,但是比较耗,现在我们就用一种空间换时间的方式来解决

 

(2)SPFA算法寻路过程

  1. 初始化各个地图块结点,设置相互之间的路径权值。
  2. 初始化dis距离向量,除起点外所有的距离设置为INT_MAX,起点本身设置为0
  3. 初始化path前驱向量,除起点外所有节点设置为-1(不可达),起点本身设置为自身的index
  4. 把起点放入队列中,然后进入循环。
  5. 循环体:把队列的元素取出(设为结点u),把U的num字段自增1,然后遍历其所有身边的结点(设为结点V)进行判断:dis【u】 + u到v的权值 < dis【v】?
  6. 若是(重点),则dis【v】赋值为dis【u】 + u到v的权值,然后把path【v】的前驱结点位置设置为u的位置,并把V结点放入队列中(若本身在队列中则不需要),进行下一结点判断
  7. 若否,则不作操作,进行下一结点判断,直至算法结束
  8. 算法结束标志有两个: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, 所以是一个负权环),答案如我们所料会进入死循环中。

 

【各寻路算法代码下载链接】

SPFA算法的分享就到这里啦,年轻人要耗子尾汁!

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lampard杰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值