负环的定义
在有向图或无向图中,存在一个环,权值之和为负数;
那么按常规最短路,就会无限在这上面更新了,也就是使得
d
i
s
t
(
u
)
=
−
I
N
F
dist(u) = -INF
dist(u)=−INF
负环的问题最经常结合01分数规划;
方法
建议使用第二种方法,一般来说,方法二跑的比较快;
方法一解释
这个方法是由 B e l l m a n − F o r d Bellman-Ford Bellman−Ford扩展过来的;
BF算法告诉我们,如果我们迭代了 n n n次,还有节点没更新,那么说明存在负环;
而我们的SPFA是基于BF算法的,在SPFA中,某个点每入队一次,就说明它被更新了一次;
因为我们每次入队都是变小的,那么更新 n n n次,自然就存在负环了;
这个方法完全等价于 B e l l m a n − F o r d Bellman-Ford Bellman−Ford的方法;
方法二解释(推荐使用)
因为我们一共就 n n n个点,但是现在已经经过了 n n n条边,即包含了 n + 1 n+1 n+1个点;
那么必然存在两个相同的点(存在环),因为SPFA只会在变小的时候更新,因此存在负环;
时间复杂度(技巧)
SPFA的时间复杂度一般是 O ( m ) O(m) O(m)的;
求负环的话,一般在 O ( n m ) O(nm) O(nm);
但是一般来说 O ( n m ) O(nm) O(nm)的时间复杂度是会超时的;
因此当我们超时的时候,我们可以考虑取一个巧;
这有一个常见的技巧,不一定对,但是效果不错;
当SPFA跑的很慢的时候,我们就可以认为存在负环了;
比如说我们可以这样,当所有点的入队次数超过 2 ∗ n 2*n 2∗n,我们就认为图中有很大可能是存在负环的;
当然这个 2 ∗ n 2*n 2∗n可以自己改,不一定正确;
方法二模板以及解释
模板
解释
为什么可以直接往队列中放入所有点?
我们可以构建一个虚拟源点,往每个点连接一条权值为 0 0 0的边;
如果原图存在负环,那么我们新增虚拟源点以后也必然存在负环,因此是等价的;
那么我们按SPFA的流程去走,放入虚拟源点,然后去更新;
我们会发现,虚拟源点会将它能到的所有点入队,而这就相当于在原图上将所有点入队的操作,我们直接省略构建虚拟源点的那一步;
为什么求负环的时候,不需要像求最短路一样,将距离设置为无穷,即 d i s t ( i ) = I N F dist(i) = INF dist(i)=INF,而是 d i s t ( i ) = 0 dist(i) = 0 dist(i)=0
如果存在负环,那么有某些点到虚拟源点的距离是 − I N F -INF −INF;
由于所有边的权值都是有限的,且负环上的点的个数也是有限的,也就是说权值和也是有限的;
因此我们无论将初值赋为多少,最终都能跑到 − I N F -INF −INF去,也就是说会跑无限次(必然超过 n n n次),因此在求负环的时候,赋值无所谓;
例题
虫洞
题面
思路
模板题,我们使用的是方法二;
Code
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <queue>
using namespace std;
typedef long long ll;
const int N = 510,M = 1e4 + 10;
int dist[N],head[N],tot;
bool vis[N];
struct Edge{
int next,to,val;
}e[M];
void add(int u,int v,int w){
++tot;
e[tot].to = v;
e[tot].val = w;
e[tot].next = head[u];
head[u] = tot;
}
int n,m1,m2;
int cnt[N];//cnt(i)表示第i个点包含的边的数量
bool spfa(){
memset(vis,0,sizeof vis);
memset(dist,0,sizeof dist);
memset(cnt,0,sizeof cnt);
queue<int> que;
for(int i=1;i<=n;++i) que.push(i);
while(!que.empty()){
int u = que.front();
que.pop();
vis[u] = 0;
for(int i=head[u];i;i=e[i].next){
int to = e[i].to,w = e[i].val;
if(dist[to] > dist[u] + w){
dist[to] = dist[u] + w;
cnt[to] = cnt[u] + 1;
if(cnt[to] >= n) return 1;
if(!vis[to]){
que.push(to);
vis[to] = 1;
}
}
}
}
return 0;
}
void solve(){
tot = 0;
memset(head,0,sizeof head);
cin >> n >> m1 >> m2;
for(int i=1,u,v,w;i<=m1;++i){
cin >> u >> v >> w;
add(u,v,w),add(v,u,w);
}
for(int i=1,u,v,w;i<=m2;++i){
cin >> u >> v >> w;
add(u,v,-w);
}
if(spfa()) cout << "YES\n";
else cout << "NO\n";
}
signed main(){
std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int t;
cin >> t;
while(t--)
solve();
return 0;
}
观光奶牛
题面
思路
题目要求我们使得 ∑ ( f i ) ∑ ( t i ) \frac{\sum(f_i)}{\sum(t_i)} ∑(ti)∑(fi)最大;
对于这类问题,我们称为0-1分数规划;
像这种问题有一个通用的解法,那就是二分;
设答案为 a n s ans ans,当前二分的值为 m i d mid mid;
假设我们现在有一个函数check
,
若 m i d ≤ a n s mid ≤ ans mid≤ans,即 m i d ≤ ∑ ( f i ) ∑ ( t i ) mid ≤ \frac{\sum(f_i)}{\sum(t_i)} mid≤∑(ti)∑(fi),那么根据二分的思想,我们应该去右区间找;
否则我们应该去左区间找;
现在我们将上面的式子变形一下,
得到 m i d ∗ ∑ ( t i ) − ∑ ( f i ) ≤ 0 { mid*{\sum(t_i) - \sum(f_i)}} ≤ 0 mid∗∑(ti)−∑(fi)≤0;
取出累加号,得到 ∑ ( m i d ∗ t i − f i ) ≤ 0 {\sum (mid*t_i-f_i)} ≤ 0 ∑(mid∗ti−fi)≤0;
至于判断 ∑ ( m i d ∗ t i − f i ) ≤ 0 {\sum (mid*t_i-f_i)} ≤ 0 ∑(mid∗ti−fi)≤0,因为我们的边是有向边,我们将点权放在出边上(放入边也行);
也就是说,我们现在将边权看成 m i d ∗ t i − f i mid*t_i-f_i mid∗ti−fi;
那么现在判断 ∑ ( m i d ∗ t i − f i ) ≤ 0 {\sum (mid*t_i-f_i)} ≤ 0 ∑(mid∗ti−fi)≤0,其实就等价于判断图上是否存在一个负环;
Code
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <queue>
using namespace std;
typedef long long ll;
const int N = 1010,M = 1e4 + 10;
double dist[N];
int head[N],tot;
bool vis[N];
struct Edge{
int next,to,val;
}e[M];
void add(int u,int v,int w){
++tot;
e[tot].to = v;
e[tot].val = w;
e[tot].next = head[u];
head[u] = tot;
}
int n,m,f[N];
int cnt[N];//cnt(i)表示第i个点包含的边的数量
const double eps = 1e-4;
bool check(double mid){
memset(cnt,0,sizeof cnt);
queue<int> que;
for(int i=1;i<=n;++i){
que.push(i);
vis[i] = 1;
}
while(!que.empty()){
int u = que.front();
que.pop();
vis[u] = 0;
for(int i=head[u];i;i=e[i].next){
int to = e[i].to;
double w = mid * e[i].val - f[u];
if(dist[to] > dist[u] + w){
dist[to] = dist[u] + w;
cnt[to] = cnt[u] + 1;
if(cnt[to] >= n) return 1;
if(!vis[to]){
que.push(to);
vis[to] = 1;
}
}
}
}
return 0;
}
void solve(){
cin >> n >> m;
for(int i=1;i<=n;++i) cin >> f[i];
for(int i=1,u,v,w;i<=m;++i){
cin >> u >> v >> w;
add(u,v,w);
}
//(l,r]
double l = 0,r = 1e3;
while(r - l > eps){
double mid = (l+r) / 2;
if(check(mid)) l = mid;
else r = mid;
}
printf("%.2f\n",r);
}
signed main(){
solve();
return 0;
}
单词环
题面
思路
假设我们将一个字符串看成一个点,然后首尾相通看成连边;
我们发现这样建图的话,最坏情况下有 1 0 5 10^5 105个长度为 1000 1000 1000的全是 a a a的字符串;
那么就有 1 0 5 10^5 105个点,以及任意两个点都有边,即 1 0 10 10^{10} 1010条边;
这样肯定是会爆空间以及时间的;
那我们将点和边反转一下,我们现在将一个字符串看成一条边,首尾两个字母看成一个点;
这样至多有 26 ∗ 26 26*26 26∗26个点, 1 0 5 10^5 105条边,这样建图的话就可以不爆时空;
然后我们发现,我们这样建图与原问题也是等价的;
即求 ∑ ( 字 符 串 长 度 ) ∑ ( 字 符 串 个 数 ) \frac{\sum(字符串长度)}{\sum(字符串个数)} ∑(字符串个数)∑(字符串长度)最大值;
这题也是0-1分数规划,那么我们按上一题的思路;
假设答案是 a n s ans ans,当前二分的为 m i d mid mid;
若 m i d ≤ a n s mid ≤ ans mid≤ans,即 m i d ≤ ∑ ( 字 符 串 长 度 ) ∑ ( 字 符 串 个 数 ) mid ≤ \frac{\sum(字符串长度)}{\sum(字符串个数)} mid≤∑(字符串个数)∑(字符串长度);
化简一下, ∑ ( m i d ∗ 个 数 − 长 度 ) ≤ 0 \sum(mid *个数 - 长度) ≤ 0 ∑(mid∗个数−长度)≤0;
同样将边权看成 m i d ∗ 个 数 − 长 度 mid *个数 - 长度 mid∗个数−长度;
也就是等价于存在负环;
那么如何判断无解呢?
要想存在负环,那么 m i d mid mid的取值是不是应该越小越好;
那 m i d mid mid的下限就是取 0 0 0,我们直接将 0 0 0代入,如果连 0 0 0都不行,那么随着 m i d mid mid增大只可能更不行;
按上面的思路走会TLE,因此加了一个上面提到的小技巧;
Code
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <queue>
using namespace std;
typedef long long ll;
const int N = 26*26 + 10,M = 1e5 + 10;
double dist[N];
int head[N],tot;
bool vis[N];
struct Edge{
int next,to,val;
}e[M];
void add(int u,int v,int w){
++tot;
e[tot].to = v;
e[tot].val = w;
e[tot].next = head[u];
head[u] = tot;
}
int n;
int cnt[N];//cnt(i)表示第i个点包含的边的数量
const double eps = 1e-4;
bool check(double mid){
memset(cnt,0,sizeof cnt);
queue<int> que;
//将所有点入队
for(int i=0;i<26*26;++i){
que.push(i);
vis[i] = 1;
}
int sum = 0;
while(!que.empty()){
int u = que.front();
que.pop();
vis[u] = 0;
for(int i=head[u];i;i=e[i].next){
int to = e[i].to;
// * 1指的是个数
double w = mid * 1 - e[i].val;
if(dist[to] > dist[u] + w){
//上面提到的技巧
//当所有点的入队次数达到某一个值的时候,差不多就是有负环了
if(++sum >= 4*26*26) return 1;
dist[to] = dist[u] + w;
cnt[to] = cnt[u] + 1;
if(cnt[to] >= 676) return 1;
if(!vis[to]){
que.push(to);
vis[to] = 1;
}
}
}
}
return 0;
}
int change(char x){
return x - 'a';
}
void solve(){
memset(head,0,sizeof head);
for(int i=1;i<=n;++i){
string str;
cin >> str;
int w = str.size();
if(w < 2) continue;
//映射成26进制数
int u = change(str[0]) * 26 + change(str[1]);
int v = change(str[w-2]) * 26 + change(str[w-1]);
add(u,v,w);
}
if(!check(0)){
cout << "No solution\n";
return;
}
//(l,r]
double l = 0,r = 1e3;
while(r - l > eps){
double mid = (l+r) / 2;
if(check(mid)) l = mid;
else r = mid;
}
printf("%.2f\n",r);
}
signed main(){
while(cin >> n,n)
solve();
return 0;
}