负环

负环是指图中出现了一个边权和为负的环。出现负环意味着该图不存在最短路,即S -> T的路径上一定可以通过一直经过负环无限缩小路径长度。

负环的判断可以使用SPFA算法。SPFA算法的特点是当一个点的最短路径发生更新时才会将点放入队列,故如果图中存在负环,则使用SPFA算法会一直进行更新,此时只需判断一个阈值,当超过阈值即可判断存在负环。

设置阈值有两种方法。第一种比较稳定但较慢,第二种不稳定但从经验角度来看一般可行。此外还存在一种优化。

第一种阈值为判断每个点最短路所包含的边数。如果某一点最短路包含边数大于等于n,则说明存在负环。

第二种阈值为判断入队次数是否大于某一值,如大于四倍点数时,可以判断存在负环。

优化则是可以将队列替换为栈,则如存在负环时,每次都会取最新更新的点判断,可以拥有更快的效率。

AcWing 904. 虫洞
题意:

给定一个无向图,以及一些有向负权边,问是否存在负环。

解析:

spfa找负环模板题,直接求解即可

代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <bits/stdc++.h>

using namespace std;
const int N = 1e3 + 10;
const int M = 1e6 + 10;
const int INF = 0x3f3f3f3f;
const long long LINF = 0x3f3f3f3f3f3f3f3f;
const int mod = 1e9 + 7;
struct node{
int u,v,w,next;
}a[M];
int f[N],cnt;
void add(int u,int v,int w){
a[++cnt].v = v;
a[cnt].w = w;
a[cnt].next = f[u];
f[u] = cnt;
}
int dis[N],Count[N],st[N];
int n,m,k;
bool spfa(){
memset(dis,0,sizeof dis);
memset(Count,0,sizeof Count);
memset(st,0,sizeof st);
queue<int> q;
for(int i = 1;i<=n;i++){
q.push(i);
st[i] = 1;
}
while(!q.empty()){
int u = q.front();
q.pop();
st[u] = 0;
for(int i =f[u];i;i = a[i].next){
int v = a[i].v,w = a[i].w;
if(dis[v] > dis[u] + w){
dis[v] = dis[u] + w;
Count[v] = Count[u] + 1;
if(Count[v] >= n) return true;
if(!st[v]){
st[v] = 1;
q.push(v);
}
}
}
}
return false;
}
int main() {
int t;
cin>>t;
while(t--){
memset(f,0,sizeof f);
cnt = 0;
cin>>n>>m>>k;
for(int i = 1;i<=m;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
add(v,u,w);
}
for(int i = 1;i<=k;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add(u,v,-w);
}
if(spfa()) puts("YES");
else puts("NO");
}
return 0;
}

01分数规划

这个知识点是在学习负环时学到的,但实际它并不只能在负环里存在。该方法通常用于求一分数的极值,如题目给出每个物品的A值和B值,问如何选择物品使得inAiinBi\frac{\sum^n_iA_i}{\sum^n_iB_i}最大。

跟二分思想相似,我们选择某一值X,寻找给定物品里是否存在inAiinBi\frac{\sum^n_iA_i}{\sum^n_iB_i}大于等于X,即inAiinBi>=X\frac{\sum^n_iA_i}{\sum^n_iB_i}>=X,而该式子又等价于inAiXinBi>=0\sum^n_iA_i-X*\sum^n_iB_i>=0。故可以使用二分查找枚举X,根据是否存在物品的集合满足上式判断是否最优。

由于该式子的性质,使得它可以在很多问题中出现,如普通的集合,或图论中都可以出现。下面这道例题就是结合了图论和01分数规划还有负环的一道模板题。

AcWing 361. 观光奶牛

题意:

给定一张 LL 个点、PP 条边的有向图,每个点都有一个权值 f[i]f[i],每条边都有一个权值 t[i]t[i]

求图中的一个环,使“环上各点的权值之和”除以“环上各边的权值之和”最大。

解析:

求一个环上t[i]f[i]\frac{\sum t[i]}{\sum f[i]}的最大值,设值X,则原式可转换为t[i]Xf[i]>=0\sum t[i]-X*\sum f[i]>=0。同时由于环上点与边的数量相等,可以将一个点的权值放在它的出边上,即该出边权值为t[i]+f[i]t[i] + f[i]。运用这种思想,讲上式取反设为边权,即每条边的边权为f[i]Xt[i]f[i]*X-t[i],即可通过spfa求负环的方式找到是否存在这样的一个环,并且可以通过二分查找不断优化答案直到求出最大值。

代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <bits/stdc++.h>

using namespace std;
const int N = 1e3 + 10;
const int M = 1e6 + 10;
const int INF = 0x3f3f3f3f;
const long long LINF = 0x3f3f3f3f3f3f3f3f;
const int mod = 1e9 + 7;
const double eps = 1e-7;
struct node{
int u,v,w,next;
}a[M];
int f[N],cnt;
void add(int u,int v,int w){
a[++cnt].v = v;
a[cnt].w = w;
a[cnt].next = f[u];
f[u] = cnt;
}
int Count[N],st[N];
double dis[N];
int val[N];
int n,m,k;
bool spfa(double mid){
for(int i = 1;i<=n;i++) dis[i] = 0;
memset(Count,0,sizeof Count);
memset(st,0,sizeof st);
queue<int> q;
for(int i = 1;i<=n;i++){
q.push(i);
st[i] = 1;
}
while(!q.empty()){
int u = q.front();
q.pop();
st[u] = 0;
for(int i =f[u];i;i = a[i].next){
int v = a[i].v;
double w = a[i].w*mid-val[u];
if(dis[v] > dis[u] + w){
dis[v] = dis[u] + w;
Count[v] = Count[u] + 1;
if(Count[v] >= n) return true;
if(!st[v]){
st[v] = 1;
q.push(v);
}
}
}
}
return false;
}

int main() {
cin>>n>>m;
for(int i = 1;i<=n;i++){
scanf("%d",&val[i]);
}
for(int i = 1;i<=m;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
}
double l = 0,r = 1000;
double ans = 0;
while(fabs(l-r)>eps){
double mid = (l+r)/2.0;
if(spfa(mid)){
ans = mid;
l = mid;
}else{
r = mid;
}
}
printf("%.2f\n",ans);
return 0;
}

差分约束

差分约束可以解决一个由n个变量组成的一元不等式组的问题,其中每个不等式为两个变量做差组成,如xjxickx_j-x_i \le c_k

我们可以对上式进行变形,得到xjxi+ckx_j\le x_i+c_k这样如果我们以xixjx_i-x_j权值为c进行建图,则如果我们在这样的图上跑一遍最短路,对于每条边的任意两点xi,xjx_i,x_j必然有dis[xj]dis[xi]+ckdis[x_j]\le dis[x_i]+c_k。这是因为在最短路中进行更新时,每条边如果满足dis[xj]>dis[xi]+ckdis[x_j]> dis[x_i]+c_k则必然会被更新。