【图论】总结 5:差分约束
差分约束系统及其解的存在性
考虑给定 \(n\) 元一次不等式组:
其给出了 \(k\) 组形如 \(x_i-x_j\le C_k\) 的不等式,其中 \(C_k\) 为常数。我们要求这个不等式组的一组可行解。这个不等式组被称作差分约束系统。
我们将每个不等式变形,可得到若干条限制 \(x_i\le x_j+C_k\),可以发现其与求最短路过程中的三角形不等式 \(dist(v)\le dist(u)+w(u,v)\) 的形式类似。因此我们可以建图来求解差分约束问题。具体而言,我们将每个约束条件 \(x_i\le x_j+C_k\) 视作连接一条有向边 \((j,i,C_k)\)。
对于差分约束问题而言,如果 \(\{x_1,x_2,\dots,x_n\}\) 是一组合法解,那么 \(\{x_1+\Delta,x_2+\Delta,\dots,x_n+\Delta\}\) 必为一组合法解,因为做差之后常数 \(\Delta\) 可以被消去。我们建立一个超级源点 \(0\),并对每个节点 \(i\) 增加有向边 \((0,i,0)\) 来模拟约束条件 \(x_i-x_0\le 0\)(即 \(x_i\le 0\)),从而得到一组负数解 \(\{x_n\}(\forall i,x_i\le 0)\)。这样在这一组负数解的基础上我们就可以得到该差分约束系统的其余可行解了。
在建立完超级源点 \(0\) 后,我们得到了一张包含 \(n+1\) 个节点以及 \(n+k\) 条有向边的有向图 \(G\)。
此时考虑差分约束系统解的存在性,不妨设图上一点 \(i\) 满足限制条件 \(x_i\le x_j+C_1\),而 \(x_j\le x_k+C_2\),我们可知 \(x_i\le x_k+C_1+C_2\),以此类推。假设图 \(G\) 上存在一个经过节点 \(i\) 的负环,那么表现为在上面的不等式放缩若干次后回到 \(i\),即:
而因为是负环,即 \(\displaystyle \sum C<0\),因而推得 \(x_i<x_i\),明显矛盾,故此时图 \(G\) 对应的差分约束系统无解。反之如果图上不存在负环,那么差分约束系统有解。
在判负环时,我们以 \(0\) 为源点跑 SPFA 即可。
差分约束系统还有一些变式:
- 若限制条件变为 \(x_i-x_j\ge C_k\Rightarrow x_i\ge x_j+C_k\),此时我们将跑最短路变为跑最长路即可,当图上存在正环时无解;
- 若限制条件变为 \(\dfrac{x_i}{x_j}\le C_k\Rightarrow x_i\le x_j\times C_k\),此时我们可以在不等式两边同时取对数,即变为 \(\log x_i\le \log(x_j\times C_k)\Rightarrow \log x_i\le \log x_j+\log C_k\),再跑最短路即可。
差分约束中的最值问题
通常对于一个差分约束问题,我们并不是想要求其一组可行解,而是想求一组满足某个值最大/小的解,这个最值是针对每个变量而言的。
首先因为要求最值,题目中必然会给出类似 \(x_i\le 0\) 的限制条件。我们以最经典的跑最短路求解的差分约束系统为例,首先考虑如何将形如 \(x_i\le c\) 这样的条件转化为图上的边。我们建立超级源点 \(0\),并令 \(x_0=0\),那么将不等式变为 \(x_i\le x_0+c\),即可图上对应一条有向边 \((0,i,c)\)。
如果我们要求 \(x_i\) 的最大值。假设有限制条件 \(x_i\le x_j+C_1\),又 \(x_j\le x_k+C_2\),等等,以此类推将不等式放缩可得到:
注意到不等号右边变成了一个确定的常数(不含任何变量),也就是一个上界的约束条件。我们要求最大值,那么也就是在若干条上界中取一个最小值,因此我们要跑最短路。
同理,如果要求最小值,则应该跑最长路。
我们总结规律:
如果要求最小值,则应该求最长路;如果要求最大值,则应该求最短路。
以 P3275 [SCOI2011] 糖果 为例(注:本题 SPFA 会被卡,这里只是做一个示范)。在 \(\forall i,x_i>0\) 的限制下,我们要求 \(\displaystyle \sum^{n}_{i=1}x_i\) 的最小值。那么我们应该跑最长路,在 SPFA 的过程中记录超级源点 \(0\) 到其余节点的最长路 \(dist(i)\),那么在有解的情况下答案即为 \(\displaystyle \sum^{n}_{i=1}dist(i)\)。
#include<bits/stdc++.h>
#define PII pair<int, int>
using namespace std;
const int N = 1e6, M = 1e6;
int n, k, mi = 1e9, ans = 0;
vector<PII> e[N];
int dist[N], cnt[N];
bool st[N];
bool spfa(int s)
{
stack<int> q;//用栈替换队列,在判负环时跑的更快一点
memset(dist, -0x3f, sizeof dist);
dist[s] = 0, cnt[s] = 0;
q.push(s);
st[s] = true;
while(q.size())
{
int u = q.top();
q.pop();
st[u] = false;
for(auto i : e[u])
{
int v = i.second, w = i.first;
if(dist[v] < dist[u] + w)
{
dist[v] = dist[u] + w;
cnt[v] = cnt[u] + 1;
if(cnt[v] >= n + 1) return true;
if(!st[v])
{
q.push(v);
st[v] = true;
}
}
}
}
return false;
}
int main()
{
cin >> n >> k;
for(int i = 1; i <= k; i ++)
{
int x, a, b;
scanf("%d%d%d", &x, &a, &b);
if(x == 1)
{
e[a].push_back({0, b});
e[b].push_back({0, a});
}
else if(x == 2) e[a].push_back({1, b});
else if(x == 3) e[b].push_back({0, a});
else if(x == 4) e[b].push_back({1, a});
else e[a].push_back({0, b});
}
for(int i = 1; i <= n; i ++) e[0].push_back({1, i});
if(spfa(0)) return puts("-1"), 0;
for(int i = 1; i <= n; i ++) ans += dist[i];
cout << ans;
return 0;
}
我们再看一道例题:
给定 \(n\) 个闭区间 \([a_i,b_i](0\le a_i,b_i\le 50000)\),要求在 \(0\sim 50000\) 中选尽可能少的数使得每个区间 \([a_i,b_i]\) 中都至少有 \(c_i\) 个数被选中。
我们设数组 \(pre[k]\) 表示 \(0\sim k\) 之间最少选多少个整数,类似于前缀和,每个限制条件等价于 \(pre[b_i]-pre[a_i-1]\ge c_i\),再加上 \(pre[k]-pre[k-1]\ge 0\) 以及 \(pre[k]-pre[k-1]\le 1\)(每个数最多只能选 \(1\) 次)的限制条件,我们可以跑最长路,那么 \(pre[50000]\) 即为答案。题目中保证了 \(c_i\le b_i-a_i+1\),因此不需要判正环。在写代码时为方便,我们将所有下标增大 \(1\)。
#include<bits/stdc++.h>
#define PII pair<int, int>
using namespace std;
const int N = 5e4 + 10;
int n;
vector<PII> e[N];
int pre[N];
bool st[N];
void spfa(int s)
{
queue<int> q;
memset(pre, -0x3f, sizeof pre);
pre[s] = 0;
q.push(s);
st[s] = true;
while(q.size())
{
int u = q.front();
q.pop();
st[u] = false;
for(auto i : e[u])
{
int v = i.second, w = i.first;
if(pre[v] < pre[u] + w)
{
pre[v] = pre[u] + w;
if(!st[v])
{
q.push(v);
st[v] = true;
}
}
}
}
}
int main()
{
cin >> n;
for(int i = 1; i <= 50001; i ++)
{
e[i - 1].push_back({0, i});
e[i].push_back({-1, i - 1});
}
for(int i = 1; i <= n; i ++)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
a ++, b ++;
e[a - 1].push_back({c, b});
}
spfa(0);
cout << pre[50001];
return 0;
}