差分约束
差分约束
1. 说明
下面的 \(x_i\) 就表示 \(dist[i]\)
怎么判断一题是不是差分约问题:看问题能不能通过一堆不等式求解
参考
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_j\) 走到 \(x_i\),长度为 \(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. 例题
题目说明
思路
很经典的差分约束模板题,我们需要将每一种情况转化为一个不等式,再把不等式转换我们希望的形式,什么是我们希望的形式?
如果让我们求最小值,我们要把所有不等式转化为: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. 差分约束+前缀和
题目描述
思路
参考题解和我的注释
代码
// 题目给定我们一堆区间[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. 差分约束+前缀和
题目描述
思路
和上一题非常相似啊。给定我们 \(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];
}
};