SPFA(Shortest Path Fast Algorithm)
某已死算法
关于SPFA,他已经死了
咳
模板
int spfa()
{
memset(dist,0x3f,sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while(q.size())
{
int t = q.front();
q.pop();
st[t] = false;
for(int i = h[t];i != -1;i ++)
{
int j = e[i];
if(dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
if(!st[j])
{
st[j] = true;
q.push(j);
}
}
}
}
return dist[n];
}
判负环
bool spfa()
{
memset(dist,0x3f,sizeof dist);
dist[1] = cnt[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
while(q.size())
{
int t = q.front();
q.pop();
st[t] = false;
for(int i = h[t];i != -1;i ++)
{
int j = e[i];
if(dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if(cnt[j] >= n) return true;
if(!st[j])
{
st[j] = true;
q.push(j);
}
}
}
}
return false;
}
流程
spfa 可以看作是 bellman_ford 的队列优化,先看一下bellman_ford;
先介绍 Bellman-Ford 算法要用到的松弛操作。
对于边 ,松弛操作对应下面的式子:\(dist(v) = min(dist(v),dist(u) + w(u,v))\)。
这么做的含义是显然的:我们尝试用 $S -> u - >v $ (其中 \(S\) 的路径取最短路)这条路径去更新 \(v\) 点最短路的长度,如果这条路径更优,就进行更新。
Bellman-Ford 算法所做的,就是不断尝试对图上每一条边进行松弛。我们每进行一轮循环,就对图上所有的边都尝试进行一次松弛操作,当一次循环中没有成功的松弛操作时,算法停止。
每次循环是 \(o(m)\) 的,那么最多会循环多少次呢?
在最短路存在的情况下,由于一次松弛操作会使最短路的边数至少 ,而最短路的边数最多为 \(n - 1\),因此整个算法最多执行 轮松弛操作。故总时间复杂度为 \(o(nm)\) 。
但还有一种情况,如果从 \(s\) 点出发,抵达一个负环时,松弛操作会无休止地进行下去。注意到前面的论证中已经说明了,对于最短路存在的图,松弛操作最多只会执行 \(n - 1\) 轮,因此如果第 \(n\) 轮循环时仍然存在能松弛的边,说明从 \(s\) 点出发,能够抵达一个负环。
不难发现,其实对于一个刚刚被松弛的点\(v\),只有他的出边才会引起松弛操作,而访问其他的点是没有必要的,所以可以用一个队列来维护所有有必要维护的点,只对有必要做松弛操作的点进行操作,由此spfa就诞生了。
技巧
一般适合写环路问题的题那么就要好好理解一下判负环模板中cnt数组的含义
[problem:LightOJ-1074]
达卡市日复一日地变得拥挤和嘈杂。某些道路总是因拥堵而受阻。为了说服人们避开最短的路线,从而避开拥挤的道路,市政府制定了新的计划。
城市的每个交汇点都标有一个正整数 (n ≤ 20),表示交汇点的繁忙程度。每当有人从一个交汇点(源交汇点)到另一个交汇点(目的地交汇点)时,市政当局就会从旅行者那里获得数量(目的地的忙碌程度 - 源的忙碌程度)3(这意味着差异的立方体)。
现在,当局已经指定您找出当智能人从某个路口(零点)到其他几个路口时可以获得的最低总金额。
输入
输入以整数 T 开头(≤ 50),表示测试用例的数量。
每个事例都包含一个空行和一个整数 n(1 < n ≤ 200),表示交汇点的数量。下一行包含 n 个整数,分别表示从 1 到 n 的交汇点的繁忙程度。
下一行包含一个整数 m,即城市中的道路数。接下来的每条 m 线(每条道路一条)都包含两个交汇点编号(源、目的地),相应的道路连接这两条交汇点编号(所有道路都是单向的)。下一行包含整数 q,即查询数。接下来的 q 行每条都包含一个目标交汇点号。从一个交叉路口到另一个交叉路口最多只能有一条直接道路。
输出
对于每个案例,请在一行中打印Case编号。然后
打印 q 行,每个查询一条,每条线都包含从交汇点 1(零点)到给定
交汇点时的最小总
收入。但是,对于总收入小于 3 的查询,
或者如果无法从零点到达目标,请打印 .?
输入示例
2
5
6 7 8 9 10
6
1 2
2 3
3 4
1 5
5 4
4 5
2
4
5
2
10 10
1
1 2
1
2
输出示例
Case 1:
3
4
Case 2:
?
分析
先抽象出题意:给定有个有向图,有q次询问,每次询问给出一个点问从1号点能否走到这个点且最小花费不小于3。这道题的边权计算有点意思,这里,我记\(p(u)\)为点u的忙碌程度\(p(v)\)为点v的忙碌程度,根据题意对于有向边 \(u -> v\) 的边权为\((p(v) - p(u)) ^ 3\),不难看出边权可为负,用spfa,同时我们思考这样一个问题,假若从起点到终点的路径中存在负环,那么,终点的最小花费一定可以任意小,可以小于3,那么必不符合题意,所以应输出”?“,
同样的,如果从起点根本走不到询问的点,那么也要输出"?"。
这里就不得不深究一下cnt数组的含义了,cnt[i]:从起点到 i 点的最短路径中边的数量。那么如果一条路径不存在环(也就是一个链),那么边数一定是节点数 - 1,如果存在环,边数一定 >= 节点数,我们只要用普通spfa来维护cnt数组即可
ac代码
#include<iostream>
#include<cstring>
#include<cstdio>
#include<queue>
#include<map>
#define ios ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
#define endl '\n'
using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
const int N = 210,M = 200010,INF = 0x3f3f3f3f;
const double INFF = 0x7f7f7f7f7f7f7f7f;
int n,m,q;
int k[N];
int dist[N];
int cnt[N];
bool st[N];
int h[N],e[M],ne[M],w[M],idx;
void add(int a,int b,int c)
{
e[idx] = b,w[idx] = c,ne[idx] = h[a],h[a] = idx ++;
}
void spfa()
{
memset(cnt,0,sizeof cnt);
memset(st,0,sizeof st);
memset(dist,0x3f,sizeof dist);
dist[1] = 0;
st[1] = true;
queue<int> q;
q.push(1);
while(q.size())
{
int t = q.front();
q.pop();
st[t] = false;
for(int i = h[t];i != -1;i = ne[i])
{
int j = e[i];
if(dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if(!st[j] && cnt[j] < n)//注意这里cnt[j] < n 才能入队的判断,如果不加上,负环上的点就会无限次入队,导致死循环
{
st[j] = true;
q.push(j);
}
}
}
}
}
int t ;
void solve()
{
memset(h,-1,sizeof h);
idx = 0;
cin >> n;
for(int i = 1;i <= n;i ++) cin >> k[i];
cin >> m;
while(m --)
{
int a,b;
cin >> a >> b;
add(a,b,(k[b] - k[a]) * (k[b] - k[a]) * (k[b] - k[a]));
}
spfa();
cin >> q;
cout << "Case " << ++ t << ":" << endl;
while(q --)
{
int p;
cin >> p;
if(cnt[p] >= n || dist[p] < 3 || dist[p] == INF) cout << "?" << endl;
else cout << dist[p] << endl;
}
}
int main()
{
ios;
int t;
cin >> t;
while(t --)
{
solve();
}
return 0;
}