Bellman-Ford算法&SPFA算法(队列优先)
(一):Bellman-Ford算法理解
Bellman – ford算法是求含负权图的单源最短路径的一种算法,效率较低,代码难度较小。其原理为连续进行松弛,在每次松弛时把每条边都更新一下,若在n-1次松弛后还能更新,则说明图中有负环,因此无法得出结果,否则就完成。
Dijkstra算法无法判断含负权边的图的最短路。如果遇到负权,在没有负权回路(回路的权值和为负,即便有负权的边)存在时,也可以采用Bellman – Ford算法正确求出最短路径。
图的任意一条最短路径既不能包含负权回路,也不会包含正权回路,因此它最多包含|v|-1条边。
其次,从源点s可达的所有顶点如果 存在最短路径,则这些最短路径构成一个以s为根的最短路径树。Bellman-Ford算法的迭代松弛操作,实际上就是按顶点距离s的层次,逐层生成这棵最短路径树的过程。
在对每条边进行1遍松弛的时候,生成了从s出发,层次至多为1的那些树枝。也就是说,找到了与s至多有1条边相联的那些顶点的最短路径;对每条边进行第2遍松弛的时候,生成了第2层次的树枝,就是说找到了经过2条边相连的那些顶点的最短路径……。因为最短路径最多只包含|v|-1条边,所以,只需要循环|v|-1 次。
每实施一次松弛操作,最短路径树上就会有一层顶点达到其最短距离,此后这层顶点的最短距离值就会一直保持不变,不再受后续松弛操作的影响。(但是,每次还要判断松弛,这里浪费了大量的时间,这就是Bellman-Ford算法效率底下的原因,也正是SPFA优化的所在)。
②:Bellman_Ford算法适用条件&范围:
1.单源最短路径(从源点s到其它所有顶点v);
2.有向图&无向图(无向图可以看作(u,v),(v,u)同属于边集E的有向图);
3.边权可正可负(如有负权回路输出错误提示);
4.差分约束系统;
(二):SPFA算法理解
SPFA(Shortest Path Faster Algorithm)(队列优化)算法是求单源最短路径的一种算法,它还有一个重要的功能是判负环(在差分约束系统中会得以体现),在Bellman-ford算法的基础上加上一个队列优化,减少了冗余的松弛操作,是一种高效的最短路算法。
只要最短路径存在,上述SPFA算法必定能求出最小值。
证明:每次将点放入队尾,都是经过松弛操作达到的。换言之,每次的优化将会有某个点v的最短路径估计值d[v]变小。所以算法的执行会使d越来越小。由于我们假定图中不存在负权回路,所以每个结点都有最短路径值。因此,算法不会无限执行下去,随着d值的逐渐变小,直到到达最短路径值时,算法结束,这时的最短路径估计值就是对应结点的最短路径值。
对SPFA的一个很直观的理解就是由无权图的BFS转化而来。在无权图中,BFS首先到达的顶点所经历的路径一定是最短路(也就是经过的最少顶点数),所以此时利用数组记录节点访问可以使每个顶点只进队一次,但在带权图中,最先到达的顶点所计算出来的路径不一定是最短路。一个解决方法是放弃数组,此时所需时间自然就是指数级的,所以我们不能放弃数组,而是在处理一个已经在队列中且当前所得的路径比原来更好的顶点时,直接更新最优解。
SPFA算法有两个优化策略SLF和LLL——SLF:Small Label First 策略,设要加入的节点是j,队首元素为i,若dist(j)<dist(i),则将j插入队首,否则插入队尾; LLL:Large Label Last 策略,设队首元素为i,队列中所有dist值的平均值为x,若dist(i)>x则将i插入到队尾,查找下一元素,直到找到某一i使得dist(i)<=x,则将i出队进行松弛操作。 SLF 可使速度提高 15 ~ 20%;SLF + LLL 可提高约 50%。 在实际的应用中SPFA的算法时间效率不是很稳定,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra算法。
适用范围:给定的图存在负权边,这时类似Dijkstra算法等便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便派上用场了。我们约定加权有向图G不存在负权回路,即最短路径一定存在。如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)。当然,我们可以在执行该算法前做一次拓扑排序,以判断是否存在负权回路。
spfa的算法思想(动态逼近法):
设立一个先进先出的队列q用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
松弛操作的原理是著名的定理:“三角形两边之和大于第三边”,在信息学中我们叫它三角不等式。所谓对结点i,j进行松弛,就是判定是否dis[j]>dis[i]+w[i,j],如果该式成立则将dis[j]减小到dis[i]+w[i,j],否则不动。
下面举一个实例来说明SFFA算法是怎样进行的:
spfa算法模板(邻接矩阵):
void spfa(int s){
for(int i=0; i<=n; i++) dis[i]=99999999; //初始化每点i到s的距离
dis[s]=0; vis[s]=1; q[1]=s; 队列初始化,s为起点
int i, v, head=0, tail=1;
while (head<tail){ 队列非空
head++;
v=q[head]; 取队首元素
vis[v]=0; 释放队首结点,因为这节点可能下次用来松弛其它节点,重新入队
for(i=0; i<=n; i++) 对所有顶点
if (a[v][i]>0 && dis[i]>dis[v]+a[v][i]){
dis[i] = dis[v]+a[v][i]; 修改最短路
if (vis[i]==0){ 如果扩展结点i不在队列中,入队
tail++;
q[tail]=i;
vis[i]=1;
}
}
}
}
(三):例题
一:https://vjudge.net/contest/173215#problem/D
D-最短路:
①:题意理解:
这里有N种货币,分别记为1~N,有M种货币交换的方式,每一种方式有A,B两种钱币,有RAB, CAB, RBA and CBA,四个数,表示交换率,Nick手上有其中的一种货币S,货币S的钱数为V,问你能否通过一定次数的钱币交换让Nick手中的钱增加
②:测试数据:
Sample Input
3 2 1 20.0 1 2 1.00 1.00 1.00 1.00 2 3 1.10 1.00 1.10 1.00
Sample Output
YES
③:Bellman_Ford算法代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n,m,s;//n可以想为银行拥有的货币种类,m为银行中可以提供货币交换的数量,s为这个人拥有的货币种类
double v,dis[101];//v为这个人拥有的货币的总量;储存每种货币的总数(本来有的和每次兑换)
struct point
{
int a;
int b;//a,b为两种交换的货币种类 a->b;
double rate ;//汇率
double commission;//佣金数
}edge[101];
bool Bellman_Ford();
int main()
{
int a,b;
double ar,ac,br,bc;
while(~scanf(“%d%d%d%lf”,&n,&m,&s,&v))
{
int c=0;//只是简单的一个变量,针对下面的两次兑换,最终a==2*m
for(int i=0;i<m;i++)
{
scanf(“%d%d%lf%lf%lf%lf”,&a,&b,&ar,&ac,&br,&bc);
//将输入的每个货币之间汇率与佣金
edge[c].a=a;
edge[c].b=b;
edge[c].rate=ar;
edge[c++].commission=ac;
//双向兑换
edge[c].a=b;
edge[c].b=a;
edge[c].rate=br;
edge[c++].commission=bc;
}
if(Bellman_Ford())
printf(“YES\n”);
else
printf(“NO\n”);
}
return 0;
}
bool Bellman_Ford()
{
memset(dis,0,sizeof(dis));//此处与Bellman-Ford的处理相反,初始化为源点到各点距离0,到自身的值为原值
dis[s]=v;//开始拥有的货币
for(int i=1;i<=n-1;i++)//n-1 轮松弛操作
{
bool flag=false;//标记是否松弛
for(int j=0;j<2*m;j++)
{
//下面的四行是赋值为了下面的松弛更简便一些
//若是没有的话就执行消去的代码
int a=edge[j].a;
int b=edge[j].b;
double ar=edge[j].rate;
double ac=edge[j].commission;
if(dis[b]<(dis[a]-ac)*ar)//松弛【也就是走这条路,钱变多】
//if(dis[edge[j].b]<(dis[edge[j].a]-edge[j].commission)*edge[j].rate)
{
//dis[edge[j].b]=(dis[edge[j].a]-edge[j].commission)*edge[j].rate;
dis[b]=(dis[a]-ac)*ar;
flag=true;
}
}
if(!flag)
break;//只能break;不能return true;这个if语句可以去掉,。
}
for(int i=0;i<2*m;i++)//正环能够无限松弛//判断是否能继续松弛,如果能,就说明有正环
if(dis[edge[i].b]<(dis[edge[i].a]-edge[i].commission)*edge[i].rate)
return true;
return false;
}
进行不停地松弛,每次松弛把每条边都更新一下,若n-1次松弛后还能更新,则说明图中有负环,无法得出结果,否则就成功完成。Bellman-ford算法有一个小优化:每次松弛先设一个flag,初值为FALSE,若有边更新则赋值为TRUE,最终如果还是FALSE则直接成功退出
④:SPFA算法代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
int n,m,s;
double v;
double dis[101],rate[1001][1001], cost[1001][1001];
bool spfa(int start)
{
bool flag[101];
//两个数组必须都要初始化
memset(dis,0,sizeof(dis));
memset(flag,0,sizeof(flag));
dis[start]=v;
//开始进行队列处理
queue <int >Q;
Q.push(start);
flag[start]=true;
while(!Q.empty())
{
int x=Q.front();
Q.pop();
flag[x]=false;
for(int i=1;i<=2*m;i++)
{
if(dis[i]<(dis[x] – cost[x][i]) * rate[x][i])
{
dis[i]=(dis[x] – cost[x][i]) * rate[x][i];
if(dis[start]>v)
return true;
if(!flag[i])
{
Q.push(i);
flag[i]=true;
}
}
}
}
return false;
}
int main()
{
int a,b;
double ar,ac,br,bc;
while(~scanf(“%d%d%d%lf”,&n,&m,&s,&v))
{
/*for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
{
if(i == j)
rate[i][j] = 1;
else
rate[i][j] = 0;
cost[i][j] = 0;
}*///不用初始化也可以;
for(int i=0;i<m;i++)
{
//读入处理输入的数
scanf(“%d%d%lf%lf%lf%lf”,&a,&b,&ar,&ac,&br,&bc);
rate[a][b] = ar;
rate[b][a] = br;
cost[a][b] = ac;
cost[b][a] = bc;
}
if(spfa(s))
printf(“YES\n”);
else
printf(“NO\n”);
}
return0;
}
两种情况YES,一种是存在正权回路;
一种是求最长路后,实现了增值,也是YES;
用spfa来判断是否存在正权回路,其实spfa是可以用来判断是否存在回路的,不管是正权还是负权,只不过它们松弛的条件不同,正权的话,我们是往dis[]权值增大的方向松弛,负权的话,我们是往dis[]权值减少的方向松弛,然后判断是否存在回路只要看有没有一点入队列的次数大于n就行了
2、NEFU 207 最小树
这个例题是从网看到的比较好的
http://blog.csdn.net/hjd_love_zzt/article/details/26739593
题目与分析:
这一道题,抽象一下,描述如下:“求从a到b的最短路径的距离”。
floyd:解决多源最短路径问题。求任意两个点之间的最短路径。这当然也就包含了“从a到b的这种情况”。所以这道题也可以使用floyd来解决
dijkstra:解决单源最短路径问题 。最典型的就是解决“从a到b的最短路径的距离”的这种问题了。
具体讲解dijkatra算法和floyd算法会在其他博客中描述,先大体看一下(代码写得很好)。
以下分别给出这两种算法的解题方法
1)使用floyd
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn = 105;
const int inf = 99999999;
int e[maxn][maxn];
int n,m;
void initial(){
int i;
int j;
for(i = 1 ; i <= n ; ++i){
for(j = 1 ; j <= n ; ++j){
if(i == j){
e[i][j] = 0;
}else{
e[i][j] = inf;
}
}
}
}
void floyd(){
int i;
int j;
int k;
for(k = 1 ; k <= n ; ++k){
for(i = 1 ; i <= n ; ++i){
for(j = 1 ; j <= n ; ++j){
if(e[i][j] > e[i][k] + e[k][j]){
e[i][j] = e[i][k] + e[k][j];
}
}
}
}
}
int main(){
while(scanf(“%d%d”,&n,&m)!=EOF){
initial();
int i;
for(i = 1 ; i <= m ; ++i){
int a,b,c;
scanf(“%d%d%d”,&a,&b,&c);
e[a][b] = e[b][a] = c;
}
floyd();
printf(“%d\n”,e[1][n]);
}
return 0;
}
2)使用dijkstra
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn = 105;
const int inf = 9999999;
int s[maxn];//用来记录某一点是否被访问过
int map[maxn][maxn];//地图
int dis[maxn];//从原点到某一个点的最短距离(一开始是估算距离)
int n;
int target;
/**
* 返回从v—->到target的最短路径
*/
int dijkstra(int v){
int i;
for(i = 1 ; i <= n ; ++i){//初始化
s[i] = 0;//一开始,所有的点均为被访问过
dis[i] = map[v][i];
}
for(i = 1 ; i < n ; ++i){
int min = inf;
int pos;
int j;
for(j = 1 ; j <= n ; ++j){//寻找目前的最短路径的最小点
if(!s[j] && dis[j] < min){
min = dis[j];
pos = j;
}
}
s[pos] = 1;
for(j = 1 ; j <= n ; j++){//遍历u的所有的邻接的边
if(!s[j] && dis[j] > dis[pos] + map[pos][j]){
dis[j] = dis[pos] + map[pos][j];//对边进行松弛
}
}
}
return dis[target];
}
int main(){
int m;
while(scanf(“%d%d”,&n,&m)!=EOF){
int i;
int j;
for(i = 1 ; i <= n ; ++i){
for(j = 1 ; j <= n ; ++j){
if(i == j){
map[i][j] = 0;
}else{
map[i][j] = inf;
}
}
}
for(i = 1 ; i <= m ; ++i){
int a,b,c;
scanf(“%d%d%d”,&a,&b,&c);
map[a][b] = map[b][a] = c;//这里默认是无向图。。所以要两个方向都做处理,只做一个方向上的处理会WA
}
target = n;
int result = dijkstra(1);
printf(“%d\n”,result);
}
return 0;
}
三、使用bellman-ford算法
bellmen-ford算法介绍:
思想:其实bellman-ford的思想和dijkstra的是很像的,其关键点都在于不断地对边进行松弛。而最大的区别就在于前者能作用于负边权的情况。其实现思路还是在求出最短路径后,判断此刻是否还能对便进行松弛,如果还能进行松弛,便说明还有负边权的边
实现:
bool bellmen_ford(){
int i;
for(i = 1 ; i <= n ; ++i){//初始化
dis[i] = inf;
}
dis[source] = 0;//源节点到自己的距离为0
int j;
for(i = 1 ; i < n ; ++i){//计算最短路径
for(j = 1 ; j <= m ; ++j){
if(dis[edge[j].v] > dis[edge[j].u] + edge[j].weight){
dis[edge[j].v] = dis[edge[j].u] + edge[j].weight;
}
if(dis[edge[j].u] > dis[edge[j].v] + edge[j].weight){
dis[edge[j].u] = dis[edge[j].v] + edge[j].weight;
}
}
}
for(j = 1 ; j <= m ; ++j){//判断是否有负边权的边
if(dis[edge[j].v] > dis[edge[j].u] + edge[j].weight){
return false;
}
}
return true;
}
基本结构:
struct Edge{
int u;
int v;
int weight;
};
Edge edge[maxm];//用来存储边
int dis[maxn];//dis[i]表示源点到i的距离.一开始是估算距离
条件:其实求最短路径的题目的基本条件都是点数、边数、起点、终点
一下给出这一道题的bellman-ford的实现方法
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn = 105;
const int maxm = 105;
struct Edge{
int u;
int v;
int weight;
};
Edge edge[maxm];//用来存储边
int dis[maxn];//dis[i]表示源点到i的距离.一开始是估算距离
const int inf = 1000000;
int source;
int n,m;
bool bellmen_ford(){
int i;
for(i = 1 ; i <= n ; ++i){//初始化
dis[i] = inf;
}
dis[source] = 0;//源节点到自己的距离为0
int j;
for(i = 1 ; i < n ; ++i){//计算最短路径
for(j = 1 ; j <= m ; ++j){
if(dis[edge[j].v] > dis[edge[j].u] + edge[j].weight){
dis[edge[j].v] = dis[edge[j].u] + edge[j].weight;
}
if(dis[edge[j].u] > dis[edge[j].v] + edge[j].weight){
dis[edge[j].u] = dis[edge[j].v] + edge[j].weight;
}
}
}
for(j = 1 ; j <= m ; ++j){//判断是否有负边权的边
if(dis[edge[j].v] > dis[edge[j].u] + edge[j].weight){
return false;
}
}
return true;
}
int main(){
while(scanf(“%d%d”,&n,&m)!=EOF){
int i;
for(i = 1 ; i <= m ; ++i){
scanf(“%d%d%d”,&edge[i].u,&edge[i].v,&edge[i].weight);
}
source = 1;
bellmen_ford();
printf(“%d\n”,dis[n]);
}
return 0;
}
四、使用spfa算法来解决。
思想:用于求单源最短路径,可以适用于负边权的情况。spfa(Shortest Path Faster Algorithm)算法其实不是什么很难理解的算法,它只是bellman-ford的队列优化而已。
模板:
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 105;
const int INF = 99999999;
int map[N][N], dist[N];
bool visit[N];
int n, m;
void init() {//初始化
int i, j;
for (i = 1; i < N; i++) {
for (j = 1; j < N; j++) {
if (i == j) {
map[i][j] = 0;
} else {
map[i][j] = map[j][i] = INF;
}
}
}
}
/**
* SPFA算法.
* 使用spfa算法来求单元最短路径
* 参数说明:
* start:起点
*/
void spfa(int start) {
queue<int> Q;
int i, now;
memset(visit, false, sizeof(visit));
for (i = 1; i <= n; i++){
dist[i] = INF;
}
dist[start] = 0;
Q.push(start);
visit[start] = true;
while (!Q.empty()) {
now = Q.front();
Q.pop();
visit[now] = false;
for (i = 1; i <= n; i++) {
if (dist[i] > dist[now] + map[now][i]) {
dist[i] = dist[now] + map[now][i];
if (visit[i] == 0) {
Q.push(i);
visit[i] = true;
}
}
}
}
}
这道题的代码如下:
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 105;
const int INF = 99999999;
int map[N][N], dist[N];
bool visit[N];
int n, m;
void init() {//初始化
int i, j;
for (i = 1; i < N; i++) {
for (j = 1; j < N; j++) {
if (i == j) {
map[i][j] = 0;
} else {
map[i][j] = map[j][i] = INF;
}
}
}
}
/**
* SPFA算法.
* 使用spfa算法来求单元最短路径
* 参数说明:
* start:起点
*/
void spfa(int start) {
queue<int> Q;
int i, now;
memset(visit, false, sizeof(visit));
for (i = 1; i <= n; i++){
dist[i] = INF;
}
dist[start] = 0;
Q.push(start);
visit[start] = true;
while (!Q.empty()) {
now = Q.front();
Q.pop();
visit[now] = false;
for (i = 1; i <= n; i++) {
if (dist[i] > dist[now] + map[now][i]) {
dist[i] = dist[now] + map[now][i];
if (visit[i] == 0) {
Q.push(i);
visit[i] = true;
}
}
}
}
}
int main(){
while(scanf(“%d%d”,&n,&m)!=EOF){
init();
while(m–){
int a,b,c;
scanf(“%d%d%d”,&a,&b,&c);
if(map[a][b] > c){
map[a][b] = map[b][a] = c;
}
}
spfa(1);
printf(“%d\n”,dist[n]);
}
return 0;
}
(四):知识点:
松弛操作:
松弛操作是指对于每个顶点v∈V,都设置一个属性d[v],用来描述从源点s到v的最短路径上权值的上界,称为最短路径估计(shortest-path estimate)。
经过初始化以后,对所有v∈V,π[v]=NIL,对v∈V-{s},有d[s]=0以及d[v]=∞。
在松弛一条边(u,v)的过程中,要测试是否可以通过u,对迄今找到的v的最短路径进行改进;如果可以改进的话,则更新d[v]和π[v]。一次松弛操作可以减小最短路径估计的值d[v],并更新v的前趋域π[v](S到v的当前最短路径中v点之前的一个点的编号)。
每个单源最短路径算法中都会调用INITIALIZE-SINGLE-SOURCE,然后重复对边进行松弛的过程。另外,松弛是改变最短路径和前趋的唯一方式。各个单源最短路径算法间区别在于对每条边进行松弛操作的次数,以及对边执行松弛操作的次序有所不同。在Dijkstra算法以及关于有向无回路图的最短路径算法中,对每条边执行一次松弛操作。在Bellman-Ford算法中,每条边要执行多次松弛操作。
这段代码是从上边复制过来的不理解变量的可以看一下上边。
for(int i=1;i<=n-1;i++)//n-1 轮松弛操作
{
bool flag=false;//标记是否松弛
for(int j=0;j<2*m;j++)
{
//下面的四行是赋值为了下面的松弛更简便一些
//若是没有的话就执行消去的代码
int a=edge[j].a;
int b=edge[j].b;
double ar=edge[j].rate;
double ac=edge[j].commission;
if(dis[b]<(dis[a]-ac)*ar)//松弛【也就是走这条路,钱变多】
//if(dis[edge[j].b]<(dis[edge[j].a]-edge[j].commission)*edge[j].rate)
{
//dis[edge[j].b]=(dis[edge[j].a]-edge[j].commission)*edge[j].rate;
dis[b]=(dis[a]-ac)*ar;
flag=true;
}
}
}
最通俗的模板:
if (d[y] > d[x] + w[x][y]){
d[y] = d[x] + w[x][y];
fa[y] = x;
}
这就是松弛;。