差分约束
1. 差分约束原理
原理
- 差分约束问题相当于给我们一组形如下面的不等式组,让我们求一组可行解:
x i ≤ x j + c k 其 中 x i 、 x j 是 自 变 量 , c k 是 常 数 x_i \le x_j+c_k \quad 其中x_i、x_j是自变量,c_k是常数 xi≤xj+ck其中xi、xj是自变量,ck是常数
注意:这里所有的不等式都是小于等于号,也可以都换成大于等于号;但不能两种都有。
- 差分约束问题可以与最短路问题或者最长路问题相互转化。如下图是转化成最短路问题,下面都是以最短路为例讲解:
差分约束
(1)求不等式的可行解:
-
源点需要满足的条件:从源点出发,一定可以走到所有的边,因为如果有边没被考虑到,相当于有些条件没考虑到。(不一定需要走到所有的点,如果一个点是孤立的,说明这个点没有任何限制;如果从一个点可以走到人以其他点,则一定可以走到所有边)。
-
步骤:
【1】先将每个不等式 x i ≤ x j + c k x_i\le x_j+c_k xi≤xj+ck转化成一条从 x j x_j xj指向 x i x_i xi且长度为 c k c_k ck的边;
【2】找一个超级源点,使得该源点一定可以遍历到所有边;
【3】从源点求一遍最短路径
结果1:如果存在负环,则原不等式组一定无解
结果2:如果没有负环,则dist[i]就是原不等式组的一个可行解
-
如果转化成图之后存在负环,那说明什么问题呢?如下图,存在负环:
根据此图可以转换为如下不等式:
x
2
≤
x
1
+
c
1
x
3
≤
x
2
+
c
2
.
.
.
x
1
≤
x
k
+
c
k
x_2 \le x_1 + c_1 \\ x_3 \le x_2 + c_2 \\ ... \\ x_1 \le x_k + c_k \\
x2≤x1+c1x3≤x2+c2...x1≤xk+ck
对
x
2
≤
x
1
+
c
1
x_2 \le x_1 + c_1
x2≤x1+c1进行缩放:
x
2
≤
x
1
+
c
1
≤
x
k
+
c
k
+
c
1
≤
x
k
−
1
+
c
k
−
1
+
c
k
+
c
1
≤
.
.
.
≤
x
2
+
∑
c
i
x_2 \le x_1 + c_1 \le x_k + c_k + c_1 \le x_{k-1}+c_{k-1} + c_k + c_1 \le ... \le x_2 + \sum c_i
x2≤x1+c1≤xk+ck+c1≤xk−1+ck−1+ck+c1≤...≤x2+∑ci
因为是负环,所以
∑
c
i
<
0
\sum c_i<0
∑ci<0,所以有
x
2
<
x
2
x_2<x_2
x2<x2,说明矛盾,原方程组不存在可行解。
结论:不等式组对应的图,如果图中存在负环,说明原方程组不存在可行解。
(2)如何求可行解中的每个变量的最大值和最小值?
-
结论:如果求的是最小值,我们应该用最长路( ≥ \ge ≥);如果求的是最大值,我们应该用最短路( ≤ \le ≤)。
-
因为我们要求解的是最值,因此不等式组不能只包含相对关系,还需要包含绝对关系,类似于 x i ≤ c x_i \le c xi≤c(这里的c是常数),那么我们如何将这类关系转化到图中呢?
方法:建立一个超级源点,不妨设为0号点,其对应的值也为0然后建立0->i,长度为c的边即可。
-
小于等于号转化成图论问题对应最短路径,因此我们求解的是每个变量的最大值,因此如果原方程组有解的话,对于 x i x_i xi一定有如下不等式链:
x i ≤ x i − 1 + c i − 1 ≤ . . . ≤ t 1 x i ≤ x i − 1 + c i − 1 ≤ . . . ≤ t 2 . . . x i ≤ x i − 1 + c i − 1 ≤ . . . ≤ t k 其 中 t 1 、 . . . 、 t k 是 c 1 、 . . . 、 c n 的 线 性 组 合 x_i \le x_{i-1} + c_{i-1} \le ... \le t_1 \\ x_i \le x_{i-1} + c_{i-1} \le ... \le t_2 \\ ... \\ x_i \le x_{i-1} + c_{i-1} \le ... \le t_k \\ 其中\quad t_1、...、t_k\quad 是\quad c_1、...、c_n\quad 的线性组合 xi≤xi−1+ci−1≤...≤t1xi≤xi−1+ci−1≤...≤t2...xi≤xi−1+ci−1≤...≤tk其中t1、...、tk是c1、...、cn的线性组合
因为这些不等式都要满足,所以
x
i
≤
m
i
n
(
t
j
)
j
=
1
,
.
.
.
,
k
x_i \le min(t_j) \quad j=1,...,k
xi≤min(tj)j=1,...,k
即最终
x
i
x_i
xi的最大值等于所有上界的最小值。
- 上面的每一个不等式链都对应图中的一条路径,如下图:
我们需要在所有这样的路径中找到一个最小值,因此求上界的最小值等价于所有从0到i的路径中的最小值,即最短路径。
- 上面的分析都是求解最大值,使用最短路径。下面讲解一下求最小值,需要使用最长路径,此时的差分约束为:
x i ≥ x j + c k 其 中 x i 、 x j 是 自 变 量 , c k 是 常 数 x_i \ge x_j+c_k \quad 其中x_i、x_j是自变量,c_k是常数 xi≥xj+ck其中xi、xj是自变量,ck是常数
- 我们需要在所有下界中找最大值,因此需要使用最长路来求解。
2. AcWing上的差分约束题目
AcWing 1169. 糖果
问题描述
-
问题链接:AcWing 1169. 糖果
分析
- 本题求最小值,因此应该使用最长路,对应不等式应该是 ≥ \ge ≥,如果存在正环,则说明无解。
- 依次分析题目中给的5个条件:
(1) A = = B ⟺ A ≥ B , B ≥ A A==B \iff A \ge B, B \ge A A==B⟺A≥B,B≥A;
(2) A < B ⟺ B ≥ A + 1 A<B \iff B \ge A + 1 A<B⟺B≥A+1;
(3) A ≥ B ⟺ A ≥ B A \ge B \iff A \ge B A≥B⟺A≥B;
(4) A > B ⟺ A ≥ B + 1 A > B \iff A \ge B + 1 A>B⟺A≥B+1;
(5) A ≤ B ⟺ B ≥ A A \le B \iff B \ge A A≤B⟺B≥A;
- 另外这个题目还有一个隐含条件:每个小朋友都要分到糖果,因此 x ≥ 1 x \ge 1 x≥1。因此我们可以建立一个虚拟源点 x 0 = 0 x_0=0 x0=0,则有: x i ≥ x 0 + 1 , i = 1 , . . . , n x_i \ge x_0+1,i=1,...,n xi≥x0+1,i=1,...,n。
- 根据不等式 x i ≥ x 0 + 1 x_i \ge x_0+1 xi≥x0+1可以建立从0号点到任意点的边,边权为1,因为可以到达任意点,所以可以到达任意边。
- 因为我们能求出每个 x i x_i xi的最小值,所以总体最小值就是所有的 x i x_i xi之和。
- 需要建立的边数:如果K取 1 0 5 10^5 105,所有的条件都是(1),需要 2 × 1 0 5 2 \times 10^5 2×105条边,同时还要从虚拟源点建立边,需要 1 0 5 10^5 105条,因此一共需要 3 × 1 0 5 3 \times 10^5 3×105条边。
- 另外如果每个小朋友的糖果数是单调递增的,则结果可能爆int,因此需要使用long long存储结果。
代码
- C++
#include <iostream>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 100010, M = 300010;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
LL dist[N];
int stk[N];
int 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++;
}
// 返回是否有解
bool spfa() {
memset(dist, -0x3f, sizeof dist);
int tt = 0; // 指向栈顶
stk[++tt] = 0;
st[0] = true;
dist[0] = 0;
while (tt > 0) {
int t = stk[tt--];
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 (cnt[j] >= n + 1) return false;
if (!st[j]) {
stk[++tt] = j;
st[j] = true;
}
}
}
}
return true;
}
int main() {
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m--) {
int x, a, b;
scanf("%d%d%d", &x, &a, &b);
if (x == 1) add(a, b, 0), add(b, a, 0);
else if (x == 2) add(a, b, 1);
else if (x == 3) add(b, a, 0);
else if (x == 4) add(b, a, 1);
else add(a, b, 0);
}
for (int i = 1; i <= n; i++) add(0, i, 1);
if (!spfa()) puts("-1");
else {
LL res = 0;
for (int i = 1; i <= n; i++) res += dist[i];
printf("%lld\n", res);
}
return 0;
}
AcWing 362. 区间
问题描述
-
问题链接:AcWing 362. 区间
分析
- 这一题是一定有解的,因为最坏的情况下我们可以把1~50000中的数据全部选上。
- 本次存在两种做法:(1)贪心;(2)差分约束。下面使用差分约束解决这个问题。
- 这里可以使用前缀和的思想求解,因为前缀和中S[0]=0,所有这里将 a i , b i a_i,b_i ai,bi所在的区间范围加上一个1,区间范围变成了[1, 50001],这样并不影响最终的结果。
- S[i]表示:1~i中被选出数的个数。我们最终要求解的就是 S 50001 S_{50001} S50001的最小值,因此需要使用最长路径。
- 对于S,S需要满足如下条件:
(1) S i ≥ S i − 1 , 1 ≤ i ≤ 50001 S_i \ge S_{i-1}, 1 \le i \le 50001 Si≥Si−1,1≤i≤50001;
(2) S i − S i − 1 ≤ 1 ⟺ S i − 1 ≥ S i − 1 S_i - S_{i-1} \le 1 \iff S_{i-1} \ge S_i - 1 Si−Si−1≤1⟺Si−1≥Si−1;
(3)区间[a, b]中至少有c个数 ⟺ S b − S a − 1 ≥ c ⟺ S b ≥ S a − 1 + c \iff S_b - S_{a - 1} \ge c \iff S_b \ge S_{a-1} + c ⟺Sb−Sa−1≥c⟺Sb≥Sa−1+c;
- 需要验证一下:从源点出发,是否一定可以走到所有的边。根据条件(1),从i-1可以走到i,因此从0可以走到1,从1可以走到2,…,因此存在这样的源点。
代码
- C++
#include <iostream>
#include <cstring>
using namespace std;
const int N = 50010, M = 150010;
int n;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
int q[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 spfa() {
memset(dist, -0x3f, sizeof dist);
int hh = 0, tt = 0;
q[tt++] = 0;
st[0] = true;
dist[0] = 0;
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];
if (!st[j]) {
q[tt++] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
}
int main() {
scanf("%d", &n);
memset(h, -1, sizeof h);
for (int i = 1; i <= 50001; i++) {
add(i - 1, i, 0);
add(i, i - 1, -1);
}
for (int i = 0; i < n; i++) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
a++, b++;
add(a - 1, b, c);
}
spfa();
printf("%d\n", dist[50001]);
return 0;
}
AcWing 1170. 排队布局
问题描述
-
问题链接:AcWing 1170. 排队布局
分析
- 因为求解最大距离,所以使用最短路径,设 x i x_i xi为第i头奶牛所在的位置。根据题目中的条件,有下列不等式:
(1) x i ≤ x i + 1 , 1 ≤ i < n x_i \le x_{i+1}, 1 \le i < n xi≤xi+1,1≤i<n;
(2) x b − x a ≤ L ⟺ x b ≤ x a + L x_b - x_a \le L \iff x_b \le x_a + L xb−xa≤L⟺xb≤xa+L;
(3) x b − x a ≥ D ⟺ x a ≤ x b − D x_b - x_a \ge D \iff x_a \le x_b - D xb−xa≥D⟺xa≤xb−D;
- 以上条件就是题目中能够得到的所有条件,只有相对关系,因此如果存在答案的话,这些位置也可以在坐标轴上平移的。
- 我们发现这里没有一个点可以到达所有点,我们也无法确定从哪个点出发可以遍历所有边,因此可以设置一个虚拟源点, x 0 = 0 x_0=0 x0=0号点,对应的值为0(即dist[0]=0)。
- 可以令 x i ≤ x 0 x_i \le x_0 xi≤x0,这样的话可以从0号点到达其他任意点,可以遍历所有的边。真实代码实现的时候,不需要将0号点建立出来,可以在spfa开始的时候将所有点入队。
- 如何判断不存在满足要求的方案?判断是否存在负环。
- 对于如何判断如果 1 号奶牛和 N 号奶牛间的距离可以任意大,我们可以固定 x 1 = 0 x_1=0 x1=0,判断最终 d i s t [ N ] dist[N] dist[N]是不是无穷大,是无穷大的话相当于1号点和N号点没有限制关系。相当于求一下1号点到N号点的最短路径。
代码
- C++
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010, M = 21010, INF = 0x3f3f3f3f;
int n, m1, m2;
int h[N], e[M], w[M], ne[M], idx;
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++;
}
bool spfa(int size) {
int hh = 0, tt = 0;
memset(dist, 0x3f, sizeof dist);
memset(st, 0, sizeof st);
memset(cnt, 0, sizeof cnt);
for (int i = 1; i <= size; i++) {
q[tt++] = i;
dist[i] = 0;
st[i] = 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] >= n) return true;
if (!st[j]) {
q[tt++] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
return false;
}
int main() {
scanf("%d%d%d", &n, &m1, &m2);
memset(h, -1, sizeof h);
for (int i = 1; i < n; i++) add(i + 1, i, 0);
while (m1--) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
if (a > b) swap(a, b);
add(a, b, c);
}
while (m2--) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
if (a > b) swap(a, b);
add(b, a, -c);
}
if (spfa(n)) puts("-1");
else {
spfa(1);
if (dist[n] == INF) puts("-2");
else printf("%d\n", dist[n]);
}
return 0;
}
AcWing 393. 雇佣收银员
问题描述
-
问题链接:AcWing 393. 雇佣收银员
分析
- 因为需要求解最小值,因此需要使用最长路。
- 我们使用num数组表示每个时刻来的人数,其中num[1]表示00:00来的人个数,num[24]表示23:00来的人的个数。
- 使用x数组表示最终从num数组中挑选的人的个数,x[1]表示从num[1]中挑选的人的个数,x[24]表示从num[24]中挑选的人的个数。
- 使用r数组表示每个时刻需要的人的个数,r[1]表示00:00需要的人的个数,r[24]表示23:00需要的人的个数。则有下列不等式:
(1) 0 ≤ x i ≤ n u m [ i ] 0 \le x_i \le num[i] 0≤xi≤num[i];
(2) x i − 7 + x i − 6 + . . . + x i ≥ r i x_{i-7} + x_{i-6} + ... + x_i \ge r_i xi−7+xi−6+...+xi≥ri;
- 上面的式子不符合差分约束中的不等式,因此需要进行变换,这里采用前缀和的技巧,令S[0]=0,S[i] = ∑ x i \sum x_i ∑xi,则我们需要求解 S 24 S_{24} S24的最小值。则上面不等式可以变为:
(1) 0 ≤ S i − S i − 1 ≤ n u m [ i ] , 1 ≤ i ≤ 24 0 \le S_i - S_{i-1} \le num[i],1 \le i \le 24 0≤Si−Si−1≤num[i],1≤i≤24;
(2) i ≥ 8 , 则 S i − S i − 8 ≥ r i i \ge 8, 则S_i - S_{i-8} \ge r_i i≥8,则Si−Si−8≥ri或者 0 < i ≤ 7 , 则 S i + S 24 − S i + 16 ≥ r i 0<i \le 7,则S_i + S_{24} - S_{i+16} \ge r_i 0<i≤7,则Si+S24−Si+16≥ri;
- 整理上述不等式得到:
(1) S i ≥ S i − 1 + 0 , 1 ≤ i ≤ 24 S_i \ge S_{i-1} + 0,1 \le i \le 24 Si≥Si−1+0,1≤i≤24;
(2) S i − 1 ≥ S i − n u m [ i ] , 1 ≤ i ≤ 24 S_{i-1} \ge S_i - num[i],1 \le i \le 24 Si−1≥Si−num[i],1≤i≤24;
(3) i ≥ 8 i \ge 8 i≥8, 则 S i ≥ S i − 8 + r i S_i \ge S_{i-8} + r_i Si≥Si−8+ri;
(4) 0 < i ≤ 7 0<i\le 7 0<i≤7,则; S i ≥ S i + 16 − S 24 + r i S_i \ge S_{i+16} - S_{24} + r_i Si≥Si+16−S24+ri;
- 我们发现第(4)个不等式不满足差分约束的不等式,因为 S 24 S_{24} S24也是变量,这里采取的策略是可以枚举该变量的所有值,这样就可以将 S 24 S_{24} S24看成常量了。因为最多有1000个人申请,所以 S 24 S_{24} S24取值范围是[0, 1000]。,假设将 S 24 S_{24} S24固定为c,则 S 24 = = c S_{24}==c S24==c,所以有下列不等式:
(5) S 24 ≥ S 0 + c S_{24} \ge S_0 + c S24≥S0+c,并且 S 0 ≥ S 24 − c S_0 \ge S_{24} - c S0≥S24−c;
- 加上0号值为0(dist[0] = 0)的点,图中一共有25个点,有70多条边,因此枚举1000多次是完全可行的。
- 另外根据不等式 S i ≥ S i − 1 + 0 , 1 ≤ i ≤ 24 S_i \ge S_{i-1} + 0,1 \le i \le 24 Si≥Si−1+0,1≤i≤24,0号点可以到达其余所有点,因此可以达到所有边。
代码
- C++
#include <iostream>
#include <cstring>
using namespace std;
const int N = 30, M = 100;
int n; // 申请岗位的人数
int h[N], e[M], w[M], ne[M], idx;
int r[N], num[N]; // num数组表示每个时刻来的人数
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;
for (int i = 1; i <= 24; i++) {
add(i - 1, i, 0);
add(i, i - 1, -num[i]);
}
for (int i = 8; i <= 24; i++) add(i - 8, i, r[i]);
for (int i = 1; i <= 7; i++) add(i + 16, i, -c + r[i]);
add(0, 24, c), add(24, 0, -c);
}
// c: 分析中的s24,返回是否存在答案
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 = 0;
q[tt++] = 0;
st[0] = true;
dist[0] = 0;
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;
}