题解:P3385 【模板】负环
UNDERTALE_RS · · 题解
P3385 【模板】负环 题解
题目传送门
前置知识
图上最短路
在图这个数据结构中任意两点间会有路径相连(或是没有,此时则没有最短路),而这两点间的最短路就是指这些路径中最短的一条。
松弛
下面给出一个例子:
在一开始时,我们会对一个点进行其它所有点距离上限的估计。在这张图中,我们关注结点
我们在一开始对于这两点的距离估计可能为
当我们看到结点
这种更新距离上限使其更接近于真实值的方式我们称作松弛。
换句话来说,就是用其它最短路的估计更新此最短路的估计,缩小它的范围。
SPFA 算法
SPFA(Shortest Path Faster Algorithm),便是求单源最短路的一种算法。
它有其优点:
- 基于 BFS 算法,易于理解。
- 便于计算负权,可直接计算。
但也有致命缺点:
- 在部分题目中,会
因为造数据人的恶意被卡,导致时间复杂度无法通过(如菊花图等)。
实现方法
首先,我们建立一个队列,会在里面放入结点。在初始时队列中只会有起始点。
同时我们建立一个数组,记录该起始点到所有点的最短路径长度(所有值通常会被赋为一个极大值,而该点本身的值为
接着,我们取出队首的结点,对它所相连的结点进行判断是否能进行松弛操作。如果有结点被松弛了并且也没有在队列中,那么就把它放入队尾。
然后,重复上面的操作直到队列为空,这时我们就计算出了起始点到所有其它的点的最短路径长度。
下面给出代码:
#include <iostream>
#include <vector>
#include <cstring>
#include <queue>
using namespace std;
const int INF = 0x3f3f3f3f; // 极大值
int dis[10005]; // 最短路径长度
struct node{
int v,w;
}; vector <node> adj[10005];
bool in_q[10005]; // 结点是否在队列
void spfa(int s){ // 起点为 s
memset(dis,INF,sizeof(dis)); // 初始化
queue <int> q;
dis[s] = 0,in_q[s] = 1,q.push(s); // 初始化起点
while(!q.empty()){
int h = q.front();
in_q[h] = 0,q.pop(); // 更改该点的状态
for(node x : adj[h])
if(dis[x.v] > dis[h] + x.w){
dis[x.v] = dis[h] + x.w; // 进行松弛
if(!in_q[x.v]) // 不在队列
in_q[x.v] = 1,q.push(x.v); // 放入队列
}
}
}
在一般情况下,SPFA 算法的时间复杂度通常为
但在稠密图边很多,或是被特殊的图卡时,会退化至
详情可见: P3371 【模板】单源最短路径(弱化版)
题目分析
看到题目,我们可以想到在 SPFA 算法中,如果从一个起点能到达一个负环,由于结点会一直被松弛,我们就无法求出最短路,而是会重复地更新最短路长度。我们就要从这里突破。
判断负环
方法 1
在 SPFA 算法的基础上,我们再添加一个统计每个结点入队次数的数组。
如果一个点入队超过了
方法 2
我们统计每两个点间的最短路包含多少条边,如果在一条最短路上包含超过了
正确性
在一开始初始化起始点时,我们其实是正确计算了从该点出发经过
可以知道,我们在第
在
同时,从一个点到任意一点的最短路必然不会超过
所以方法 2 可行。
每个结点入队次数其实就是这个结点被松弛的次数。
所以如果在第
所以方法 1 可行。
代码实现
这里使用方法 2 实现判负环。
#include <iostream>
#include <cstring>
#include <vector>
#include <queue>
using namespace std;
const int INF = 0x3f3f3f3f; // 极大值
int T,n,m,q,dis[2005],cnt[2005]; // cnt 记录最短路径经过边数
bool in_q[2005];
struct node{
long long v,w;
}; vector <node> adj[2005];
bool spfa(int s){
memset(cnt,0,sizeof(cnt)); // 初始化计数数组
queue <int> q;
q.push(s),in_q[s] = 1,dis[s] = 0; // 初始化起点
while(!q.empty()){
int h = q.front();
q.pop(),in_q[h] = 0;
if(cnt[h] > n) // 判断最短路边数是否大于 n,通常会因为添加超级源点写为 > 而并非 >=
return 1; // 有负环
for(node x : adj[h])
if(dis[x.v] > dis[h] + x.w){ // 可以松弛
dis[x.v] = dis[h] + x.w; // 进行松弛
cnt[x.v] = cnt[h]+1; // 边数为对它松弛的点的边数+1
if(!in_q[x.v]) // 不在队中
in_q[x.v] = 1,q.push(x.v); // 入队
}
}
return 0; // 成功计算出最短路,故没有负环
}
int main(){
cin >> T;
while(T--){
memset(adj,0,sizeof(adj));
memset(in_q,0,sizeof(in_q));
memset(dis,INF,sizeof(dis));
// 多测时包括图也要初始化!!!
cin >> n >> m;
while(m--){ // 建图
int u,v,w;
cin >> u >> v >> w;
if(w >= 0)
adj[u].push_back({v,w}),adj[v].push_back({u,w});
else
adj[u].push_back({v,w});
}
cout << (spfa(1)?"YES\n":"NO\n");
}
return 0;
}
上述代码时间复杂度为
拓展部分
本题只要求我们求“从点
这个超级源点会与所有节点间存在一条边权为
在开始时便以
推荐练习
P1938 [USACO09NOV] Job Hunt S
总结
本题为负环模板题,考察图数据结构以及最短路算法,同时负环在差分约束系统中起重要作用。
感谢您的阅读!