CSP-Week7 ProblemC SPFA和负环判定

本文介绍了SPFA(Shortest Path Faster Algorithm)算法,它是对Bellman-Ford算法的一种优化,用于解决带负权边的单源最短路径问题。通过建立队列,每次从队首取出节点松弛其邻接点,以此提高效率。文章还提供了SPFA的具体实现代码,并结合一个具体的实例题目,解释了如何运用SPFA求解最少税费问题,并注意在出现负环时的处理方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

CSP-SPFA和负环判定

基础知识

在讲述SPFA之前,我们需要了解的是他的根源算法Bellman-Ford算法。
Bellman-Ford算法
在一个正权图中,求解单源最短路可以使用Dijkstra算法解决,并且具有较好的时间复杂度O((m+n)logn)。但是在很多实际问题中,会出现很多负权边的情况,一旦出现负边dijkstra算法无法保证原先的正确性,Bellman和Ford引入了新的算法来计算带负权的单源最短路求解算法。
在保证Bellman-Ford算法的正确性之前,需要理解如下的事实:
1、如果图中有n个点,单源最短路径经过的边数<点数
2、如果在路径上边数等于或者超过了n,说明某个点被访问了两次,出现了环路
3、当有一条边(u,v),如果dis[u]是最小值,且(u,v)在dis[v]的路径上,那么可以将dis[v]更新成dis[u]+(u,v)的权值
由此我们可以使用暴力模拟法得出Bellman-Ford算法的求解过程:
因为最短路的边最长为n-1,所以调整n-1次,每次调整过程中遍历所有边,保证第i次调整结束后,长度为i的边最小值已经被确定。时间复杂度为O(nm)
Bellman-Ford算法具体实现代码:

struct edge
{
    int x;
    int y;
    int value;
};
edge edges[1000];
for(int i=0;i<=n;i++)
{
    dis[i]=inf;
    pre[i]=0;
}
dis[s]=0;
//更新n-1趟,每次更新全部边,并不够优秀
for(int k=1;k<n;k++)
    for(int i=0;i<=m;i++)
    {
        if(dis[edges[i].y)>dis[edges[i].x]+edges[i].value)
        {
            dis[edges[i].y]=dis[edges[i].x]+edges[i].value;
            pre[edges[i].y]=edges[i].x;
        }

    }

虽然这种方法是可行的,但由于边数m的大小未知,在求解稠密图问题时算法很有可能退化到O(n^3),因此我们需要一种较好的优化方式来修改这种算法。
SPFA队列优化
如果你多读两遍上面的代码并且画出他的具体执行图,就会发现Bellman-Ford的执行过程与BFS十分相似,都是一种在图上的扩散最终遍历到能够到达的所有点。因此我们可以思考能否引入BFS中的队列思想来对Bellman-Ford做以优化。
对Bellman-Ford进行实例测试我们可以发现这样的特点:松弛操作只发生在最短路径的前导节点中,已经松弛过的点上。
可能你听的有些乱,那我们举个例子:
第一次松弛:松弛与源点相连的边,找到所有边长为1的最短路
第二次松弛:第一轮被松弛过点作为前导节点松弛,找到所有边长为2的最短路

可以发现一个特点:只有在前面被松弛过的点后面才会作为先导节点再次进行松弛。
利用这个特点,类比队列,就可以得到SPFA的实现方法:
1、建立一个队列,队列存储要进行松弛的点,初始化将源点放入到队列中
2、每次从队首取点并松弛其邻接点,如果一个邻接点被松弛且队列中无该元素,则将其加入队列
3、在每次松弛结束后进行判断该条路径上的边数,一旦边数>=n,说明重复访问了某个点,出现了负环。对该点打上标记,并且将于该点联通的所有点都打上标记。在进行后续的松弛时,带有负环标记的点不可以被加入队列,否则程序将死循环。
4、重复2-3至队列为空。
SPFA利用了Bellman-Ford算法的松弛性质,在正常数据情况下,大大提高了性能,时间复杂度为 O(km)(k是一个常数)。
SPFA的具体实现:

queue<int> q;
int cnt[N];
int vis[N];
int pre[N];
int dis[N];
void SPFA(int s)
{
    while(q.size()) q.pop();
    //q置空
    for(int i=1;i<=n;i++)
    { cnt[i]=0,dis[i]=inf,pre[i]=0;vis[i]=0;}
    pre[s]=s;
    cnt[s]=0;
    dis[s]=0;
    vis[s]=1;//证明这个点在松弛队列中
    q.push(s);
    while(q.size())
    {
        int x=q.front();
        q.pop();
        vis[x]=0;
        //x出队,则将vis[x] 重置为0
        for(int i=head[x];i;i=e[i].nxt)
        {
            int y=e[i].to;
            int value=e[i].value;
            if(dis[y]>dis[x]+value)//松弛
            {
                dis[y]=dis[x]+value;
                pre[y]=x;
                cnt[y]=cnt[x]+1;
                if(cnt[y]>=n)//负环
                {
                    dfs(y);//找到负环就dfs负环点,都标记为-inf
                    //标记到的这些点永远不能进队列
                }
                if(vis[y]==0)//如果队列中没有这个点,就加入到队列中
                {
                    vis[y]=1;
                    q.push(y);
                }
            }
        }
    }
}

题目概述

这一晚,TT 做了个美梦!
在梦中,TT 的愿望成真了,他成为了喵星的统领!喵星上有 N 个商业城市,编号 1 ~ N,其中 1 号城市是 TT 所在的城市,即首都。
喵星上共有 M 条有向道路供商业城市相互往来。但是随着喵星商业的日渐繁荣,有些道路变得非常拥挤。正在 TT 为之苦恼之时,他的魔法小猫咪提出了一个解决方案!TT 欣然接受并针对该方案颁布了一项新的政策。
具体政策如下:对每一个商业城市标记一个正整数,表示其繁荣程度,当每一只喵沿道路从一个商业城市走到另一个商业城市时,TT 都会收取它们(目的地繁荣程度 - 出发地繁荣程度)^ 3 的税。
TT 打算测试一下这项政策是否合理,因此他想知道从首都出发,走到其他城市至少要交多少的税,如果总金额小于 3 或者无法到达请悄咪咪地打出 ‘?’。

INPUT&输入样例

第一行输入 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 号点的最少税费。
输入样例:

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

OUTPUT&输出样例

每个询问输出一行,如果不可达或税费小于 3 则输出 ‘?’。
输出样例:

Case 1:
3
4
Case 2:
?
?

题目重述

题目是SPFA的板子题,但是有两点需要注意:
1、给定的并非边权而是每个点的值,要使用题目中给定的方式求出边权
2、在出现负环时DFS函数的编写。

思路概述

按照SPFA详解进行输入输出的处理即可完成题目。

问题源码(C++)

#include<iostream>
#include<stdio.h>
#include<queue>
#include<vector>
#include<string>
using namespace std;

const int M=1e6+1;
const int N=1e3+1;
const int inf=1e8+1;
int point_number;//点个数
int edge_number;
int tot=0;
int point_value[N];//点价值

int head[N];
struct Edge
{
    int to,nxt,value;
}e[M];
void init()
{
    for(int i=0;i<=point_number;i++)
    head[i]=-1;
}
void add(int x,int y)
{
    tot++;
    e[tot].to=y;
    e[tot].value=(point_value[y]-point_value[x])*(point_value[y]-point_value[x])*(point_value[y]-point_value[x]);
    e[tot].nxt=head[x];
    head[x]=tot;
}

int vis[N];//记录是否在队列里
int cnt[N];//记录到该点使用的边数
int dis[N];//记录到该点最短的距离
int flag[N];//记录该点是否是环路影响到的点
int dfs_vis[N];//记录dfs过程中是否被访问过
queue<int> q;

void dfs(int s)
{
    if(dfs_vis[s]==1)
    return;
    else
    {
        dfs_vis[s]=1;
        flag[s]=1;
        for(int i=head[s];i;i=e[i].nxt)
        {
            dfs(e[i].to);
        }
    }
}
void SPFA(int s)
{
    //初始化
    for(int i=1;i<=point_number;i++)
    {
        vis[i]=0;
        cnt[i]=0;
        dis[i]=inf;
        flag[i]=0;
    }
    while(!q.empty()) q.pop();
    vis[s]=1,cnt[s]=0,dis[s]=0,flag[s]=0;
    q.push(s);
    while(!q.empty())
    {
        int x=q.front();
        q.pop();
        vis[x]=0;
        for(int i=head[x];i;i=e[i].nxt)
        {
            int y=e[i].to;
            int value=e[i].value;
            //松弛
            if(dis[y]>dis[x]+value)
            {
                dis[y]=dis[x]+value;
                cnt[y]=cnt[x]+1;
                if(cnt[y]>=point_number)//如果松弛后边数已经>=n,说明成环
                {
                    for(int i=1;i<=point_number;i++)
                    dfs_vis[i]=0;
                    dfs(y);
                }
                if(vis[y]==0 && flag[y]==0)//如果已经在队列中且不是环路点,加入队列
                {
                    vis[y]=1;
                    q.push(y);
                }
            }
        }
    }
}

int main()
{
    int group=0;
    scanf("%d",&group);
    int cout_case=1;
    for(int j=0;j<group;j++)
    {
        //初始化
        tot=0;
        scanf("%d",&point_number);
        for(int i=0;i<=point_number;i++)
        point_value[i]=0;
        //输入
        for(int i=1;i<=point_number;i++)
        {
            scanf("%d",&point_value[i]);
        }
        scanf("%d",&edge_number);
        init();
        for(int i=1;i<=edge_number;i++)
        {
            int x,y;
            scanf("%d %d",&x,&y);
            add(x,y);
        }
        /*for(int i=1;i<=point_number;i++)
        {
            for(int k=head[i];k;k=e[k].nxt)
    			printf("%d %d %d->",i,e[k].to,e[k].value);
            cout<<endl;
        }*/
        SPFA(1);
        /*for(int i=1;i<=point_number;i++)
        {
        	printf("%d ",dis[i]);
		}
		printf("\n");
		for(int i=1;i<=point_number;i++)
        {
        	printf("%d ",flag[i]);
		}
		printf("\n");*/
		printf("Case %d:\n",cout_case);
		cout_case++;
        int check_number=0;
        scanf("%d",&check_number);
        for(int i=1;i<=check_number;i++)
        {
        	int check;
        	scanf("%d",&check);
        	if(dis[check]<3 || flag[check]==1 || dis[check]==inf)
        	printf("?\n");
        	else
        	printf("%d\n",dis[check]);
		}
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值