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;
}