差分约束

差分约束

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是常数 xixj+ckxixjck

注意:这里所有的不等式都是小于等于号,也可以都换成大于等于号;但不能两种都有。

  • 差分约束问题可以与最短路问题或者最长路问题相互转化。如下图是转化成最短路问题,下面都是以最短路为例讲解:

在这里插入图片描述


差分约束

(1)求不等式的可行解:

  • 源点需要满足的条件:从源点出发,一定可以走到所有的边,因为如果有边没被考虑到,相当于有些条件没考虑到。(不一定需要走到所有的点,如果一个点是孤立的,说明这个点没有任何限制;如果从一个点可以走到人以其他点,则一定可以走到所有边)。

  • 步骤:

    【1】先将每个不等式 x i ≤ x j + c k x_i\le x_j+c_k xixj+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 \\ x2x1+c1x3x2+c2...x1xk+ck
x 2 ≤ x 1 + c 1 x_2 \le x_1 + c_1 x2x1+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 x2x1+c1xk+ck+c1xk1+ck1+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 xic(这里的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 的线性组合 xixi1+ci1...t1xixi1+ci1...t2...xixi1+ci1...tkt1...tkc1...cn线

因为这些不等式都要满足,所以
x i ≤ m i n ( t j ) j = 1 , . . . , k x_i \le min(t_j) \quad j=1,...,k ximin(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是常数 xixj+ckxixjck

  • 我们需要在所有下界中找最大值,因此需要使用最长路来求解。

2. AcWing上的差分约束题目

AcWing 1169. 糖果

问题描述

分析

  • 本题求最小值,因此应该使用最长路,对应不等式应该是 ≥ \ge ,如果存在正环,则说明无解。
  • 依次分析题目中给的5个条件:

(1) A = = B    ⟺    A ≥ B , B ≥ A A==B \iff A \ge B, B \ge A A==BAB,BA

(2) A < B    ⟺    B ≥ A + 1 A<B \iff B \ge A + 1 A<BBA+1

(3) A ≥ B    ⟺    A ≥ B A \ge B \iff A \ge B ABAB

(4) A > B    ⟺    A ≥ B + 1 A > B \iff A \ge B + 1 A>BAB+1

(5) A ≤ B    ⟺    B ≥ A A \le B \iff B \ge A ABBA

  • 另外这个题目还有一个隐含条件:每个小朋友都要分到糖果,因此 x ≥ 1 x \ge 1 x1。因此我们可以建立一个虚拟源点 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 xix0+1i=1,...,n
  • 根据不等式 x i ≥ x 0 + 1 x_i \ge x_0+1 xix0+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. 区间

问题描述

分析

  • 这一题是一定有解的,因为最坏的情况下我们可以把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 SiSi1,1i50001

(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 SiSi11Si1Si1

(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 SbSa1cSbSa1+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. 排队布局

问题描述

分析

  • 因为求解最大距离,所以使用最短路径,设 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 xixi+1,1i<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 xbxaLxbxa+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 xbxaDxaxbD

  • 以上条件就是题目中能够得到的所有条件,只有相对关系,因此如果存在答案的话,这些位置也可以在坐标轴上平移的。
  • 我们发现这里没有一个点可以到达所有点,我们也无法确定从哪个点出发可以遍历所有边,因此可以设置一个虚拟源点, x 0 = 0 x_0=0 x0=0号点,对应的值为0(即dist[0]=0)。
  • 可以令 x i ≤ x 0 x_i \le x_0 xix0,这样的话可以从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. 雇佣收银员

问题描述

分析

  • 因为需要求解最小值,因此需要使用最长路。
  • 我们使用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] 0xinum[i]

(2) x i − 7 + x i − 6 + . . . + x i ≥ r i x_{i-7} + x_{i-6} + ... + x_i \ge r_i xi7+xi6+...+xiri

  • 上面的式子不符合差分约束中的不等式,因此需要进行变换,这里采用前缀和的技巧,令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 0SiSi1num[i]1i24

(2) i ≥ 8 , 则 S i − S i − 8 ≥ r i i \ge 8, 则S_i - S_{i-8} \ge r_i i8,SiSi8ri或者 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<i7Si+S24Si+16ri

  • 整理上述不等式得到:

(1) S i ≥ S i − 1 + 0 , 1 ≤ i ≤ 24 S_i \ge S_{i-1} + 0,1 \le i \le 24 SiSi1+01i24;

(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 Si1Sinum[i]1i24;

(3) i ≥ 8 i \ge 8 i8, 则 S i ≥ S i − 8 + r i S_i \ge S_{i-8} + r_i SiSi8+ri;

(4) 0 < i ≤ 7 0<i\le 7 0<i7,则; S i ≥ S i + 16 − S 24 + r i S_i \ge S_{i+16} - S_{24} + r_i SiSi+16S24+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 S24S0+c,并且 S 0 ≥ S 24 − c S_0 \ge S_{24} - c S0S24c

  • 加上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 SiSi1+01i24,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;
}
  • 6
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值