差分约束

差分约束

1. 说明

下面的 \(x_i\) 就表示 \(dist[i]\)
怎么判断一题是不是差分约问题:看问题能不能通过一堆不等式求解
参考

WIKI
AcWing




2. 应用

差分约束可以转换成最短路问题

如果不等式的形式应该严格满足:\(xi <= xj + C\),其中 \(x_i\)\(x_j\) 是变量,\(C\) 是常数
假设在图中有一条边,\(j \rightarrow i\)\(dist[j][i] = C\), 则有三角不等式dist[i] <= dist[j] + C
可以发现差分约束的条件就等于三角不等式

在最短路问题中,随意选取一个起点,求这个起点到其他所有点的最短距离,最后图中的每一条边(可以遍历到)都是满足差分约束的
当然起点不可以乱取,只有这个起点可以走到所有的边,这个起点才是满足条件的。




求不等式的可行解

一般步骤:
\([1]\) 先将每个不等式 \(x_i <= x_j + C\) 变成一条从 \(x_i\) 走到 \(x_j\),长度为 \(C\) 的一条边
\([2]\) 找一个虚拟源点,使得该源点一定可以遍历到所有边。如果有些边遍历不到,就说明有些条件满足不到
\([3]\) 从源点求一遍单源最短路。如果图中存在负环,则原不等式组一定无解,否则,原不等式组一定存在可行解,注意在最长路中无解等价于存在正环




求最值(违反直觉)

这里的最值指的是每个变量的最值
首先,求最值问题的必要条件是题目中存在一个绝对条件,例如:\(x_i <= C\), \(x_i >= C\),只有这样我们才能找到一个依据,只有相对关系是无法求最值的
因为我们最后肯定可以把所有不等式变成一个不等式链:
\(x_1 <= x_2 + c_1 <= x_3 + c_1 + c_2 <= ... <= x_0 + c_1 + c_2 + ... + c_n\)
如果最后的 \(x_0\) 是一个变量,那么就无法求最值,而如果 \(x_0\) 是一个常数的话,就可以求最值了

那么问题来了,如何转化绝对条件呢?
方法:建立一个虚拟源点( \(0\) 号点),把 \(x_i <= C\) 转化成 \(x_i <= x_0 + C\),因为 \(x_0 = 0\),对应于图中就是一条从 \(0\)\(i\) 的长度是 \(0\) 的一条边。

求最小值:最长路

为什么求最小值要用最长路呢?
看下面的不等式:

x >= 1;
x >= 2;
x >= 5;

如果我们要求 x 的最小值,那么显然是最大的 5 了。你可能会问,万一不等式是下面这样的呢?

x <= 1;
x <= 2;
x <= 5;
这种情况下无法求最小值,这不会这样问你的!

求最大值:最短路

同理。

3. 例题

题目说明

模板题
Reference

思路

很经典的差分约束模板题,我们需要将每一种情况转化为一个不等式,再把不等式转换我们希望的形式,什么是我们希望的形式?
如果让我们求最小值,我们要把所有不等式转化为:A >= B + K 的形式
因为只有在此情况下才能求得最小值,如果是:A <= B + K 的形式,就不存在最小值了。
另外,由于最小值的不等式形式为 :A >= B + K,所以我们要求最大值。
如果边权都是正值,可以用 \(spfa\) 求最长路,如果边权都是负值,可以用 \(dijkstra\) 求最长路。
所以本题用 \(spfa\) 算法。
但是在 \(spfa\) 时,我们需要判断,是否存在环,因为存在环时,肯定不存在解。
在判断负环的时候,我们可以使用 stack 而不是 queue 来加快负环的出栈,如果是队列的话需要等一轮。
另外就是,我们需要通过虚拟源点把所有条件串起来,否则条件与条件之间没有关联的话,也就是图不连通,我们无法求最长路。

代码

#include <iostream>
#include <cstring>
#include <algorithm>
#include <stack>

#define IOS ios_base::sync_with_stdio(false), cin.tie(0), cout.tie(0)

using namespace std;

const int N = 1e5 + 10, M = 3 * N, INF = 0x3f3f3f3f;

int n, m;
int h[N], e[M], ne[M], w[M], idx;
int dist[N], cnt[N];
bool st[N];

// 求最小值,不等式变形为 >=
// 只有变为 >= 才能求得最小值
// 而 >= 就要求最长路

void add(int a, int b, int c)
{
    w[idx] = c;
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

bool spfa()
{
    // 通过栈,可以加快负环的判断,避免TLE
    stack<int> q;
    // 因为是求最长路,所以初始化为最小值
    memset(dist, -0x3f, sizeof dist);
    // 0 是超级源点
    // 添加虚拟源点的边
    // 小朋友所在点 >= 虚拟源点 + 1
    // 之所以 +1.是因为每个小朋友至少一个糖果
    for(int i = 1; i <= n; i ++ )   add(0, i, 1);
    q.push(0);
    st[0] = true;
    dist[0] = 0;
    

    while(q.size())
    {
        auto t = q.top(); q.pop();
        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 true;
                if(!st[j])
                {
                    q.push(j);
                    st[j] = true; 
                }
            }
        }
    }
    return false;
}

int main()
{
    IOS;
    memset(h, -1, sizeof h);
    cin >> n >> m;
    while (m -- )
    {
        int op, a, b;
        cin >> op >> a >> b;
        // 转化不等式
        if(op == 1) // A == B <==> A >= B && B >= A
            add(a, b, 0), add(b, a, 0);
        else if(op == 2) // A < B <==> B >= A + 1
            add(a, b, 1);    
        else if(op == 3) // A >= B <==> A >= B
            add(b, a, 0);
        else if(op == 4) // A > B <==> A >= B + 1 
            add(b, a, 1);
        else if(op == 5) // A <= B <==> B >= A
            add(a, b, 0);
    }
    if(spfa())  puts("-1");
    else
    {
        long long res = 0;
        for(int i = 1; i <= n; i ++ )   res += dist[i];
        cout << res << endl;
    }
    return 0;
}

4. 差分约束+前缀和

题目描述

AcWing 362

思路

参考题解和我的注释

代码

// 题目给定我们一堆区间[a,b]和每个区间的限制c
// 让我们构造一个集合S,满足S在[a,b]范围内数的个数>=c
// 我们假设S是一个bool数组,数组范围就是max{ai,bi}
// 其中,s[0]=1表示这个数在集合中,反之,不在
// 我们可以对S求前缀和,那么只需要满足:s[b]-s[a-1]>=c即可
// 另外,在前缀和中,有 s[i]>=s[i-1]
// 本题比较特殊,有 s[i] <= s[i-1]+1
// 因为从i到i+1最多会多一个数,就是i+1
// 有了着三个不等式,我们就可以求最长路得到最小值了

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 50010, M = N * 3, T = 50001;

int h[N], e[M], ne[M], w[M], idx;
bool st[N];
int dist[N];

void add(int a, int b, int c)
{
    w[idx] = c, e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

void spfa()
{
    queue<int> q;
    memset(dist, -0x3f, sizeof dist); // 求最长路,初始化最小值
    q.push(0);
    dist[0] = 0;    // 前缀和的头作为虚拟源点
    st[0] = true;
    while(q.size())
    {
        auto t = q.front(); q.pop();
        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];
                if(!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    
}

int main()
{
    memset(h, -1, sizeof h);
    int n;   cin >> n;
    int maxn = 0;
    while(n -- )
    {
        int a, b, c;
        cin >> a >> b >> c;
        a ++ , b ++ ;   // 因为需要用到前缀和,让起下标从0快开始
        // s[b] - s[a - 1] >= c
        // s[b] >= s[a - 1] + c
        add(a - 1, b, c);
        maxn = max(b, maxn);
    }
    
    for(int i = 1; i <= maxn; i ++ )
    {
        // s[i]>=s[i-1]
        add(i - 1, i, 0);
        // s[i]-s[i-1]<=1. --> s[i-1]>=s[i]-1
        add(i, i - 1, -1);
    }
    
    spfa();
    cout << dist[maxn] << endl;
    return 0;
}

5. 差分约束+前缀和

题目描述

LeetCode
Reference

思路

和上一题非常相似啊。给定我们 \(n\) 个区间,上一题是保证区间内至少有 \(k\) 个满足条件的数,这一题也是是保证区间内至少有 \(k\) 个满足条件(在工作)的数。
然后让我们求最小值,等价于求最长路,正权,\(spfa\),一气呵成!
在前缀和数组 \(s\) 中,肯定有如下不等式:
s[i] >= s[i - 1]
另外,本题和上一题一样特殊,在前缀和中都有如下条件:
s[i] - s[i - 1] <= 1
最后一个条件也一毛一样。给定区间 [l, r],有:
s[r] - s[l - 1] >= k

代码

// 本题一定有解,不需要判断环
// 因为最小值,所以要将不等式变形为 >= 的形式
const int N = 2010, M = N * 3;
int h[N], e[M], ne[M], w[M], idx;
int dist[N];
bool st[N];
queue<int> q;

class Solution {
public:
    void add(int a, int b, int c) {
        w[idx] = c, e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
    }
    void spfa() {
        memset(dist, -0x3f, sizeof dist);   // 求最小值,用最长路初始化
        dist[0] = 0;    // 下标从1开始的前缀和,下标为0处恒为0
        q.push(0);
        st[0] = true;
        while(q.size()) {
            auto t = q.front(); q.pop();
            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];
                    if(!st[j]) {
                        q.push(j);
                        st[j] = true;
                    }
                }
            }
        }
    }
    void init() {
        // LC有点恶心,如果不初始化idx,他会是一个随机数,好恶心啊
        idx = 0;
        memset(h, -1, sizeof h);
        memset(e, 0, sizeof e);
        memset(ne, 0, sizeof ne);
        memset(st, 0, sizeof st);
        memset(w, 0, sizeof w);
    }
    int findMinimumTime(vector<vector<int>>& tasks) {
        // 初始化
        init();
        // 1. 建图
        int rare = -1;
        for(auto &node : tasks) {
            int l = node[0], r = node[1], c = node[2];
            // s[r] - s[l - 1] >= c
            // s[r] >= s[l - 1] + c
            add(l - 1, r, c);
            rare = max(rare, r);
        }
        // s[i] >= s[i - 1]
        // s[i] - s[i - 1] <= 1  ---> s[i - 1] >= s[i] - 1
        for(int i = 1; i <= rare; i ++ ) {
            add(i - 1, i, 0);
            add(i, i - 1, -1);
        }
        // 2. spfa
        spfa();
        return dist[rare];
    }
};
posted @ 2022-06-12 23:31  光風霽月  阅读(15)  评论(0编辑  收藏  举报