title: 【算法提高课】图论:SPFA找负环
katex: true
tags:
- Acwing
- medium
- 图论
categories: 算法提高课
概论
- 求不等式组的可行解
- 需要满足的条件: 从源点出发,一定可以走到所有的边
- 步骤(以最短路为例):
- 先将每个不等式 x i ≤ x j + c k x_i \leq x_j+c_k xi≤xj+ck 转化成一条从 x j x_j xj 走到 x i x_i xi ,长度为 c k c_k ck 的一条边
- 找一个超级源点,使得该源点一定可以遍历到所有的边
- 从源点求一遍单源最短路
结果1:如果存在负环,那么原不等式一定无解
结果2: 如果没有负环,那么dis[i]
就是原不等式的一个可行解
- 如何求最大值或最小值(这里的最值指的是每个变量的最值:
- 结论: 如果求的是最小值,应求最长路。如果求最大值,应求最短路。举例理解:
dis[j]<dis[t]+w[i]
这是最短路符合的关系式,那左边的是不是很像最大值? - 问题: 如何转化 x i ≤ c x_i \leq c xi≤c,其中 c c c 为常数,这类的不等式
- 方法: 建立一个超级源点,
0
0
0,然后建立
0
−
>
i
0->i
0−>i ,长度为
c
c
c 的边即可
以求 x i x_i xi 的最大值为例,求所有从 x i x_i xi 出发,构成的不等式链 x i ≤ x j + c 1 ≤ x k + c 2 + c 1 ≤ . . . ≤ c 1 + c 2 + c 3 . . . + c i x_i \leq x_j+c_1 \leq x_k+c_2+c_1 \leq ... \leq c_1+c_2+c_3...+c_i xi≤xj+c1≤xk+c2+c1≤...≤c1+c2+c3...+ci 所计算出的上界,最终 x i x_i xi 的最大值等于所有上界的最小值(取并集)
- 结论: 如果求的是最小值,应求最长路。如果求最大值,应求最短路。举例理解:
- 注意: 差分约束的链式法则在不等式链的末端一定会是一个常数
Acwing.1169 糖果
- 题意: 有
N
(
1
e
5
)
N(1e5)
N(1e5) 个小朋友,在分糖果时需要满足
K
(
1
e
5
)
K(1e5)
K(1e5) 个要求,每个小朋友都至少要分到一个糖果,求满足要求所需的最小糖果数量。
要求如下:- A 和 B 分到的一样多
- A 比 B 多
- A 不少于 B
- B 不少于 A
- B 比 A 多
- 思路: 题目有一个个不等式:说明本题为差分约束。题目要求最少需要准备多少个糖果:对应差分约束的最小求最长路。那么只需要将题目的不等式转化即可:注意本题为最长路,和上面的最短路举例建边是相反的。简单的判断方法:b比a多所以b要在a后面(因为最长路)。
- 雷点: 数据范围。本题用队列会 T。用栈则不会
- C o d e Code Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
typedef long long ll;
int head[N],to[N],nxt[N],w[N],idx;
ll dis[N];
int cnt[N],n,m;
bool st[N];
ll res;
void add(int u,int v,int val){
to[++idx]=v,w[idx]=val,nxt[idx]=head[u],head[u]=idx;
}
bool spfa(){
memset(dis,-0x3f,sizeof dis);
stack<int>stk;
dis[0]=0;
stk.push(0);
st[0]=true;
while(stk.size()){
int t=stk.top();
stk.pop();
st[t]=false;
for(int i=head[t];i;i=nxt[i]){
int j=to[i];
if(dis[j]<dis[t]+w[i]){
dis[j]=dis[t]+w[i];
cnt[j]=cnt[t]+1;
if(cnt[j]>=n+1) return false;
if(!st[j]){
stk.push(j);
st[j]=true;
}
}
}
}
return true;
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int a,b,c;
cin>>c>>a>>b;
if(c==1) add(a,b,0),add(b,a,0);
else if(c==2) add(a,b,1);
else if(c==3) add(b,a,0);
else if(c==4) add(b,a,1);
else add(a,b,0);
}
for(int i=1;i<=n;i++) add(0,i,1);
// spfa();
if(!spfa()) puts("-1");
else{
for(int i=1;i<=n;i++){
res+=dis[i];
// cout<<dis[i]<<endl;
}
cout<<res;
}
return 0;
}
Acwing.362 区间
- 题意: 给定
n
(
5
e
4
)
n(5e4)
n(5e4) 个区间
[
a
i
,
b
i
]
[a_i,b_i]
[ai,bi] 和
n
n
n 个整数
c
i
c_i
ci。你需你需要构造一个整数集合 Z
,使得 ∀ i ∈ [ 1 , n ] ∀i∈[1,n] ∀i∈[1,n], Z Z Z中满足 a i ≤ x ≤ b i a_i≤x≤b_i ai≤x≤bi的整数 x x x 不少于 c i c_i ci个。求这样的整数集合 Z Z Z 最少包含多少个数。 - 思路: 考虑前缀和
S
i
S_i
Si 表示区间
[
1
,
i
]
[1,i]
[1,i] 所包含的整数数量。由此我们可以得出下列不等式
- S i ≥ S i − 1 S_i ≥ S_{i-1} Si≥Si−1 :前缀和性质
- S i − S i − 1 ≤ 1 S_i-S_{i-1}≤1 Si−Si−1≤1:后一项比前一项最多大1
- S b − S a − 1 ≥ c S_b-S_{a-1}≥c Sb−Sa−1≥c:区间 [ a , b ] [a,b] [a,b] 至少有 c c c 个数,前缀和思想
- C o d e Code Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int idx,nxt[N],to[N],w[N],head[N];
int dis[N],n,m;
bool st[N];
void add(int u,int v,int val){
to[++idx]=v,w[idx]=val,nxt[idx]=head[u],head[u]=idx;
}
void spfa(){
queue<int>q;
q.push(0);
memset(dis,-0x3f,sizeof dis);
dis[0]=0;
while(q.size()){
int t=q.front();
q.pop();
st[t]=false;
for(int i=head[t];i;i=nxt[i]){
int j=to[i];
if(dis[j]<dis[t]+w[i]){
dis[j]=dis[t]+w[i];
if(!st[j]){
q.push(j);
st[j]=true;
}
}
}
}
}
int main(){
cin>>n;
for(int i=1;i<=50001;i++){
add(i-1,i,0);
add(i,i-1,-1);
}
for(int i=1;i<=n;i++){
int a,b,c;
cin>>a>>b>>c;
a++,b++;
add(a-1,b,c);
}
spfa();
cout<<dis[50001];
return 0;
}
Acwing.1170 排队布局
- 题意: 给出
N
(
1000
)
N(1000)
N(1000) 头牛,编号从
1
1
1 到
N
N
N ,牛在队伍的顺序和他们的编号是相同的,可能有多条奶牛站在一个位置上。想象奶牛站在一条数轴上,给定以下关系
- 两只奶牛相对位置不小于 L L L
- 两只奶牛相对位置不大于
D
D
D
求 1 1 1 号奶牛到 N N N 号奶牛的最大距离,若不存在输出 − 1 -1 −1,无限大输出 − 2 -2 −2
- 思路: 题目已经有两个不等式,而且隐含一个不等式:顺序和编号是相同的
- C o d e Code Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
const int INF=0x3f3f3f3f;
int head[N],nxt[N],to[N],w[N],idx;
int cnt[N],dis[N];
bool st[N];
int n,m,M;
void add(int u,int v,int val){
to[++idx]=v,nxt[idx]=head[u],w[idx]=val,head[u]=idx;
}
bool spfa(int pos){
memset(cnt,0,sizeof cnt);
memset(dis,0x3f,sizeof dis);
memset(st,false,sizeof st);
queue<int>q;
for(int i=1;i<=pos;i++){
q.push(i);
dis[i]=0;
st[i]=true;
}
while(q.size()){
int t=q.front();
q.pop();
st[t]=false;
for(int i=head[t];i;i=nxt[i]){
int j=to[i];
if(dis[j]>dis[t]+w[i]){
dis[j]=dis[t]+w[i];
cnt[j]=cnt[t]+1;
if(cnt[j]>=n) return true;
if(!st[j]){
q.push(j);
st[j]=true;
}
}
}
}
return false;
}
int main(){
cin>>n>>m>>M;
for(int i=1;i<n;i++){
add(i+1,i,0);
}
while(m--){
int a,b,c;
cin>>a>>b>>c;
if(a>b) swap(a,b);
add(a,b,c);
}
while(M--){
int a,b,c;
cin>>a>>b>>c;
if(a>b) swap(a,b);
add(b,a,-c);
}
if(spfa(n)) puts("-1");
else{
spfa(1);
if(dis[n]>INF/2) puts("-2");
else cout<<dis[n]<<endl;;
}
return 0;
}
Acwing.393 雇佣收银员
- 题意: 一家超市二十四小时营业,现给出 0 - 23 小时每一小时时间段所需的收银员个数,并给出 N N N 位收银员和他们的开始值班时间,每个收银员会值班八个小时,求最少需要雇佣多少名收银员
- 思路: 和第二题类似,利用前缀和的思想维护信息,本题前缀和表示前多少小时招多少人。本题有一个特殊的点是数据是环形的,前缀和比较特殊。其中分类讨论过程中会出现一个 S 24 S_{24} S24 ,这个是我们要求的数,所以我们直接枚举即可
- 偷懒:
- 由于牵扯到
[
i
−
8
+
1
,
i
]
[i - 8 + 1, i]
[i−8+1,i] 这段区间的和的约束,所以用前缀和更好表达一些。
设 n u m [ i ] num[i] num[i] 表示 i i i 时刻有多少人申请上岗, x [ i ] x[i] x[i] 为 i i i 时刻实际上岗的人数 , s s s 为 x x x 的前缀和数组。
则应该满足的约束条件是:
上岗人数不能负数,即
s
[
i
]
−
s
[
i
−
1
]
>
;
=
0
s[i] - s[i - 1] \gt;= 0
s[i]−s[i−1]>;=0
实际上岗人数不能超过申请人数,即
s
[
i
]
−
s
[
i
−
1
]
<
=
n
u
m
[
i
]
s[i] - s[i - 1] \lt= num[i]
s[i]−s[i−1]<=num[i]
i
i
i 时刻所在人数,即
[
i
−
7
,
i
]
[i - 7, i]
[i−7,i] 区间内的上岗人数要大于等于最小需求
R
R
R
由于存在环,即 23 23 23 到 24 24 24,再到 0 0 0 时刻,所以要分类讨论
当 KaTeX parse error: Expected 'EOF', got '&' at position 3: i &̲gt;= 8 时,KaTeX parse error: Expected 'EOF', got '&' at position 17: …[i] - s[i - 8] &̲gt;= R[i]
当 KaTeX parse error: Expected 'EOF', got '&' at position 3: i &̲lt;= 7 时,
s
[
i
]
+
s
[
24
]
−
s
[
16
+
i
]
>
=
R
[
i
]
s[i] + s[24] - s[16 + i] \gt= R[i]
s[i]+s[24]−s[16+i]>=R[i]
显然这是一个明显的差分约束问题,由于求最小人数,所以用最长路转化:
s
[
i
]
>
=
s
[
i
−
1
]
s[i] \gt= s[i - 1]
s[i]>=s[i−1] 即
a
d
d
(
i
−
1
,
i
,
0
)
add(i - 1, i, 0)
add(i−1,i,0)
s
[
i
]
−
n
u
m
[
i
]
<
=
s
[
i
−
1
]
s[i] - num[i] \lt= s[i - 1]
s[i]−num[i]<=s[i−1] 即
a
d
d
(
i
,
i
−
1
,
−
n
u
m
[
i
]
)
add(i, i - 1, -num[i])
add(i,i−1,−num[i])
s
[
i
−
8
]
+
R
[
i
]
<
=
s
[
i
]
s[i - 8] + R[i] \lt= s[i]
s[i−8]+R[i]<=s[i] 即
a
d
d
(
i
−
8
,
i
,
R
[
i
]
)
add(i - 8, i, R[i])
add(i−8,i,R[i])
s
[
16
+
i
]
+
R
[
i
]
−
s
[
24
]
<
=
s
[
i
]
s[16 + i] + R[i] - s[24] \lt= s[i]
s[16+i]+R[i]−s[24]<=s[i],不会连边了hhhh
最后一种约束关系我们不会连边的原因无非是出现了三个变量,但我们可以发现:
所有最后一种约束关系都有 s [ 24 ] s[24] s[24] 变量,其实这个东西就是我们求的答案,所以我们可以枚举 s [ 24 ] s[24] s[24] 的值,把它变成常量就行啦!然后就可以 a d d ( 16 + i , i , R [ i ] − s [ 24 ] ) add(16 + i, i, R[i] - s[24]) add(16+i,i,R[i]−s[24])
T i p s : Tips: Tips:
关于建图,其实可以在线建图,不用僵化建边了嘿嘿。
发现
0
0
0 肯定所有点,所以不用创造超级源点了,只需从
0
0
0 点出发跑最短路即可。
不要忘了 $s[24] = $ 我们枚举的数
c
c
c(要严格等于,实现是 大于等于 + 小于等于):
KaTeX parse error: Expected 'EOF', got '&' at position 7: s[24] &̲lt;= c 即 $add(24, 0, -c)
KaTeX parse error: Expected 'EOF', got '&' at position 7: s[24] &̲gt;= c 即
a
d
d
(
0
,
24
,
c
)
add(0, 24, c)
add(0,24,c)
时间复杂度
这个题中的点数 $ <= 26$,边数 $ <= 26 * 3 = 78$,
所以时间复杂度
O
(
T
n
2028
)
O(Tn2028)
O(Tn2028),足以
A
C
AC
AC
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 30, M = 100, INF = 0x3f3f3f3f;
int n;
int h[N], e[M], w[M], ne[M], idx;
int r[N], num[N];
int dist[N];
int q[N], cnt[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
void build(int c)
{
memset(h, -1, sizeof h);
idx = 0;
add(0, 24, c), add(24, 0, -c);
for (int i = 1; i <= 7; i ++ ) add(i + 16, i, r[i] - c);
for (int i = 8; i <= 24; i ++ ) add(i - 8, i, r[i]);
for (int i = 1; i <= 24; i ++ )
{
add(i, i - 1, -num[i]);
add(i - 1, i, 0);
}
}
bool spfa(int c)
{
build(c);
memset(dist, -0x3f, sizeof dist);
memset(cnt, 0, sizeof cnt);
memset(st, 0, sizeof st);
int hh = 0, tt = 1;
dist[0] = 0;
q[0] = 0;
st[0] = true;
while (hh != tt)
{
int t = q[hh ++ ];
if (hh == N) hh = 0;
st[t] = false;
for (int i = h[t]; ~i; 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 (cnt[j] >= 25) return false;
if (!st[j])
{
q[tt ++ ] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
return true;
}
int main()
{
int T;
cin >> T;
while (T -- )
{
for (int i = 1; i <= 24; i ++ ) cin >> r[i];
cin >> n;
memset(num, 0, sizeof num);
for (int i = 0; i < n; i ++ )
{
int t;
cin >> t;
num[t + 1] ++ ;
}
bool success = false;
for (int i = 0; i <= 1000; i ++ )
if (spfa(i))
{
cout << i << endl;
success = true;
break;
}
if (!success) puts("No Solution");
}
return 0;
}