SPFA思路讲解,如何判断最短路中是否存在负权环路,例题(计算过路税收)。c++

Bellman-Ford和SPFA思路

  • 在解单源最短路径的时候第一个想法是dijkstra,但是dijkstra存在一定的局限性,图中存在负权边的时候没有办法保证它的正确性,为了解决相应的问题,使用bellman-ford算法和他的队列优化spfa来进行单源最短路径的计算
  • bellman-ford算法会给出源点s到图内所有点的最短路和对应的前驱子图,如果最短路经过的路径条数大于等于点的个数,意味着存在某点被重复经过
  • 当松弛边(u,v)时,如果dis[u]已经是最短路并且u在v的最短路上,那么松弛结束后的dis[v]也是最短路,并且之后dis值不会发生变化
  • bellman-ford算法比较暴力,对每一条边进行松弛,共松弛 (点数-1 ) 轮,松弛完第 i 轮后所有经过 i 条边的最短路均被确定
for(int i=1;i<=n;i++)
{
dis[i]=inf;pre[i]=0;
}
dis[s]=0;
for(int k=1;k<n;k++)
for(int i=1;i<=m;i++)
if(dis[v[i]]>dis[u[i]]+w[i])//松弛
{
dis[v[i]]=dis[u[i]]+w[i];
pre[v[i]]=u[i];}
  • bellman-ford算法解决了负权边的问题,但是复杂度是O(mn),对于算法中无效的松弛操作可以进行优化:松弛操作仅发生在最短路径前导节点中已经成功松驰过的节点上,所以可以建立一个队列,存储被成功松弛的点,每次front()取出成功松弛的点并松弛他的邻接点,松弛成功则放入队列
  • 这个时候又出现了新的问题,最短路一定会存在吗?如果源点s没有办法到达,那么最短路就不会存在;如果图中存在负环,那么一直沿着负环走,最短路长度就会越来越小,没有意义
  • 所以如果图中可能出现负权边,负环的话,我们就要首先进行判断!
  • 存在负环的话最短路经过的边数会超过点数;一些边会被多次松弛。根据这两个性质就可以得到图中存在负环的判断条件:如果在n次松弛的时候还有边能被成功松弛,就说明图中存在负环!
  • 由于在SPFA中很难判断这个点加入了队列多少次,这条边被松弛了多少次,所以可以判断最短路的边数,如果某一点的最短路边数超过了n-1,说明存在负环;所以可以创建一个数组cnt[x]来表示x当前最短路上的边数,如果cnt[x]大于等于点数就说明图中存在负环
if(dis[v]>dis[u]+e.w)
{
cnt[v]=cnt[u]+1;//加入一条新的边,cnt++
if(cnt[v]>=n)//找到负环
}
  • 既然利用v点能找到负环,那么就说明v在负环上,则我们可以利用bfs找出这个图中所有在负环上的点,标记他们,在最终输出的时候作为一个判断条件

下面用一个例题来看一下SPFA的实际运用情况


税收计算
某地有 N 个商业城市,编号 1 ~ N,其中 1 号城市是 TT 所在的城市,即首都。

该地共有 M 条有向道路供商业城市相互往来。但是随着商业的日渐繁荣,有些道路变得非常拥挤,为解决相关问题将颁布一项政策。

具体政策如下:对每一个商业城市标记一个正整数,表示其繁荣程度,当每个人沿道路从一个商业城市走到另一个商业城市时,该地都会收取它们(目的地繁荣程度 - 出发地繁荣程度)^ 3 的税。

现在需要测试一下这项政策是否合理,TT想知道从首都出发,走到其他城市至少要交多少的税,如果总金额小于 3 或者无法请输出?

第一行输入 T,表明共有 T 组数据。(1 <= T <= 50)

对于每一组数据,第一行输入 N,表示点的个数。(1 <= N <= 200)

第二行输入 N 个整数,表示 1 ~ N 点的权值 a[i]。(0 <= a[i] <= 20)

第三行输入 M,表示有向道路的条数。(0 <= M <= 100000)

接下来 M 行,每行有两个整数 A B,表示存在一条 A 到 B 的有向道路。

接下来给出一个整数 Q,表示询问个数。(0 <= Q <= 100000)

每一次询问给出一个 P,表示求 1 号点到 P 号点的最少税费

每个询问输出一行,如果不可达或税费小于 3 则输出 '?‘

sample input:
2
5
6 7 8 9 10
6
1 2
2 3
3 4
1 5
5 4
4 5
2
4
5
10
1 2 4 4 5 6 7 8 9 10
10
1 2
2 3
3 1
1 4
4 5
5 6
6 7
7 8
8 9
9 10
2
3 10

sample output:
Case 1:
3
4
Case 2:
?
?

思路

  • 利用上面讲到的SPFA的方式来查找,最终输出的值需要进行判断:是否在负环上,是否dis小于3,是否之间的路并不存在(dis值仍为初始值INF)
  • 整型数count记录最短路中的点的个数,利用bool型数组circle来标记点是否在负权环路中
#include<iostream>
#include<cstdio> 
#include<cstring>
#include<queue>
#include<vector>
#include<cmath>
const int INF=10000000;
const int N=1001;
using namespace std;
struct edge
{
 int u,v,w,next;
}edges[100001];
int head[222];
int a[222];
int cnt[N];
int pre[N];
int dis[N];
bool circle[N],visit[N];
int count=0;
int n;
void add(int u,int v,int w)
{
 edges[count].u=u;
 edges[count].v=v;
 edges[count].w=w;
 edges[count].next=head[u];
 head[u]=count;
 count++;
}
void bfs(int s)
{
 queue<int> q;
 q.push(s);
 while(!q.empty())
 {
  int temp=q.front();
  q.pop();
  circle[temp]=true;
  for(int i=head[temp];i!=-1;i=edges[i].next)
  {
   if(!circle[edges[i].v])
    q.push(edges[i].v);
  }
 }
}
void SPFA(int s)
{
 for(int i=1;i<=n;i++)
 {
  dis[i]=INF;
  pre[i]=0;
  cnt[i]=0;
  visit[i]=false;
  circle[i]=false;
 }
 dis[s]=0;
 visit[s]=true;
 queue<int>q;
 q.push(s);
 while(!q.empty())
 {
  int temp=q.front();
  q.pop();
  if(circle[temp])
   continue;
  visit[temp]=false;
  for(int i=head[temp];i!=-1;i=edges[i].next)
  {
   if(dis[edges[i].v]>dis[edges[i].u]+edges[i].w)//存在负环 
   {
    cnt[edges[i].v]=cnt[edges[i].u]+1;
    if(cnt[edges[i].v]>=n)
    {
     bfs(edges[i].v);
    }
    dis[edges[i].v]=dis[edges[i].u]+edges[i].w;
    pre[edges[i].v]=edges[i].u;
    if(!visit[edges[i].v])
    {
     q.push(edges[i].v);
     visit[edges[i].v]=true;
    }
   }
   } 
 }
}
int main()
{
 int t;
 scanf("%d",&t);
 for(int i=0;i<t;i++)
 {
  scanf("%d",&n);
  for(int j=1;j<=n;j++)
  {
   head[j]=-1;
   scanf("%d",&a[j]);
  }
  int m,q,p;
  scanf("%d",&m);
  count=0;
  for(int j=0;j<m+1;j++)
  {
   edges[j].u=0;
   edges[j].v=0;
   edges[j].w=0;
   edges[j].next=0;
  }
  for(int j=0;j<m;j++)
  {
   int x,y;
   scanf("%d%d",&x,&y);
   int z=pow((a[y]-a[x]),3);
   add(x,y,z);
  }
  SPFA(1);
  scanf("%d",&q);
  printf("Case %d:\n",i+1);
  for(int j=0;j<q;j++)
  {
   cin>>p;
   if(dis[p]==INF||dis[p]<3||circle[p])
    {
    printf("?\n");
    continue;
       }
   printf("%d\n",dis[p]);
  }
 }
 return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值