数学和图论的完美结合——差分约束
由于作者觉得差分约束很好玩,所以心血来潮写了个记录。
Part 0:前置芝士
- 最短路(SPFA);
- 负环;
- 一些数学知识。
Part 1:什么是差分约束?
其实差分约束跟差分没有什么关系。
我们将形如这样的不等式组称为差分约束系统:
其中包含
一般的,我们的任务就是找到这个不等式组的一组整数解。
举个例子:
我们很容易就能找到一组解,比如
我们称
Part 2:如何求解差分约束?
Part 2.1:最短路解法
我们首先可以将不等式组转换一下。
对于一个不等式组其中的一个不等式
学过 SPFA 的都知道,SPFA 算法中有一个性质,就是三角不等式。对于起点
你会发现,这些变量可以全部对应。那么,差分约束是否可以用最短路解决呢?答案是可以。
那么我们容易得出以下最短路连边方法:
::::success[结论 1.1]{open}
对于一个不等式
那么我们可以把每一个未知量都看成一个顶点。此外,我们为了保证图联通,我们需要建立一个超级源点,一般情况下为
一般情况下,在有解的时候,不等式组的解如下:
::::success[结论 2.1]{open}
对于一个不等式组的未知量
这时候你肯定会问,那么怎么判定无解呢?
你可能会猜想,负环就是无解的情况。我们先别急于解决,我们先举个例子。
例如这组不等式组:
我们先来解一下这个不等式组。
由
很显然,
解完不等式组后,我们进行连边:
我们发现,图中确实存在负环。
于是我们的得到结论 3:
::::success[结论 3.1]{open} 对于一组不等式组,在用最短路解决的情况下,如果构图后图中有负环,那么不等式组无解。 ::::
有人会问,这只是举了个例子,有没有详细一点的?
当然,作者用另外一种方法讲一下:由于
什么,你不知道怎么判负环?我来讲讲,因为 SPFA 每次会将一个点放进队列里,并对这个点所有地出边进行松弛。如果这个点地进队次数超过了
所以,我们只需要记录每个点的入队次数并进行判断即可。
Part 2.2:最长路算法
欸,作者?你前文提到了最短路,那最长路可以解决吗?最长路也可以的。
我们将不等式变成另外一个形式:
由最短路算法同理可得以下结论:
::::success[结论 1.2]{open}
对于一个不等式
::::success[结论 2.2]{open}
对于一个不等式组的未知量
::::success[结论 3.2]{open} 对于一组不等式组,在用最长路解决的情况下,如果构图后图中有正环,那么不等式组无解。 ::::
Part 3:例题
P5960 【模板】差分约束
模板题,按照步骤解决即可。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 5e6 + 10;
int n, m, s, v[kMaxN], cnt[kMaxN], dis[kMaxN];
vector<pair<int, int>> e[kMaxN];
string SPFA() {
fill(dis, dis + n + 1, 2147483647);
queue<int> q;
dis[0] = 0, v[0] = 1;
for (q.push(0); q.size(); ) {
int x = q.front();
q.pop();
v[x] = 0;
for (auto i : e[x]) {
if (dis[i.first] > dis[x] + i.second) {
dis[i.first] = dis[x] + i.second;
if (v[i.first]) {
continue;
}
v[i.first] = 1;
q.push(i.first);
++ cnt[i.first];
if (cnt[i.first] > n) {
return "NO";
}
}
}
}
return "YES";
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; ++ i) {
e[0].push_back({i, 0});
}
for (int i = 1, u, v, w; i <= m; ++ i) {
cin >> u >> v >> w;
e[v].push_back({u, w});
}
string t = SPFA();
if (t == "NO") {
cout << t << '\n';
} else {
for (int i = 1; i <= n; ++ i) {
cout << dis[i] << ' ';
}
cout << '\n';
}
return 0;
}
P1993 小 K 的农场
首先将题目的三种形式转换成不等式形式:
前两种很好解决,那么第三种呢?很简单,只需转换成
于是不等式移项并变形后变成这样:
那么接下来就很好解决了。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 5e6 + 10;
int n, m, s, v[kMaxN], cnt[kMaxN], dis[kMaxN];
vector<pair<int, int>> e[kMaxN];
string SPFA() {
fill(dis, dis + n + 1, 2e9);
queue<int> q;
dis[0] = 0, v[0] = 1;
for (q.push(0); q.size(); ) {
int x = q.front();
q.pop();
v[x] = 0;
for (auto i : e[x]) {
if (dis[i.first] > dis[x] + i.second) {
dis[i.first] = dis[x] + i.second;
if (v[i.first]) {
continue;
}
v[i.first] = 1;
q.push(i.first);
++ cnt[i.first];
if (cnt[i.first] > n) {
return "No";
}
}
}
}
return "Yes";
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; ++ i) {
e[0].push_back({i, 0});
}
for (int i = 1, op, u, v, w; i <= m; ++ i) {
cin >> op;
if (op == 1) {
cin >> u >> v >> w;
e[u].push_back({v, -w});
} else if (op == 2) {
cin >> u >> v >> w;
e[v].push_back({u, w});
} else {
cin >> u >> v;
e[u].push_back({v, 0});
e[v].push_back({u, 0});
}
}
cout << SPFA() << '\n';
return 0;
}
P2294 [HNOI2005] 狡猾的商人
其实前面那一段文字都是没有什么用的。
显然,看到总收入和一段时间,我们就可以想到前缀和数组
所以,我们将从第
同样的,根据上一题的解法,我们可以把这个式子拆成两个不等式:
变形,得:
于是我们只需要连两条边:
这里有一个特别需要注意的点,
改完之后还要注意,点数会变为
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 5e6 + 10;
int n, m, s, v[kMaxN], cnt[kMaxN], dis[kMaxN];
vector<pair<int, int>> e[kMaxN];
string SPFA() {
fill(dis, dis + n + 2, 2147483647);
queue<int> q;
dis[0] = 0, v[0] = 1;
for (q.push(0); q.size(); ) {
int x = q.front();
q.pop();
v[x] = 0;
for (auto i : e[x]) {
if (dis[i.first] > dis[x] + i.second) {
dis[i.first] = dis[x] + i.second;
if (v[i.first]) {
continue;
}
v[i.first] = 1;
q.push(i.first);
++ cnt[i.first];
if (cnt[i.first] > n + 1) {
return "false";
}
}
}
}
return "true";
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
int t;
for (cin >> t; t; -- t) {
cin >> n >> m;
for (int i = 1; i <= n + 1; ++ i) {
e[0].push_back({i, 0});
}
for (int i = 1, u, v, w; i <= m; ++ i) {
cin >> u >> v >> w;
++ u, ++ v;
e[u - 1].push_back({v, w});
e[v].push_back({u - 1, -w});
}
cout << SPFA() << '\n';
for (int i = 1; i <= n + 1; ++ i) {
e[i].clear();
cnt[i] = v[i] = 0;
}
}
return 0;
}
Part 4:差分约束的变式
由于篇幅太长,所以这里只写一个典型的变式问题。
因为这些问题没有太系统化的总结,所以我们用例题 P4878 [USACO05DEC] Layout G 来讲一讲。
第一眼:差分约束模板。
第二眼:
先别急,我们先来建边。
首先看到最大距离,就知道要跑最短路了。
设第
没有合法方案的情况很简单,我们先从
重点来了,我们需要跑两次最短路,第二次需要从
那跑完了之后干啥?这里我们就需要知道一个结论了:
::::success[结论 4.1]{open}
对于一个不等式组,在有解的情况下,
欸?有时候
那么这道题就做完了。但是,这里作者要增加一个结论。我们通过结论 4.1 对称及推导之后可以得到:
::::success[结论 4.2]{open}
对于一个不等式组,在有解的情况下,
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
const int kMaxN = 5e6 + 10;
int n, ml, md, v[kMaxN], cnt[kMaxN], dis[kMaxN];
vector<pair<int, int>> e[kMaxN];
bool SPFA(int s) {
fill(dis, dis + n + 1, 2e9);
queue<int> q;
dis[s] = 0, v[s] = cnt[s] = 1;
for (q.push(s); q.size(); ) {
int x = q.front();
q.pop();
v[x] = 0;
for (auto i : e[x]) {
if (dis[i.first] > dis[x] + i.second) {
dis[i.first] = dis[x] + i.second;
if (v[i.first]) {
continue;
}
v[i.first] = 1;
q.push(i.first);
++ cnt[i.first];
if (cnt[i.first] >= n) {
return 0;
}
}
}
}
return 1;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> ml >> md;
for (int i = 1; i <= n; ++ i) {
e[0].push_back({i, 0});
}
for (int i = 1, u, v, w; i <= ml; ++ i) {
cin >> u >> v >> w;
e[u].push_back({v, w});
}
for (int i = 1, u, v, w; i <= md; ++ i) {
cin >> u >> v >> w;
e[v].push_back({u, -w});
}
for (int i = 1; i < n; ++ i) {
e[i + 1].push_back({i, 0});
}
if (!SPFA(0)) {
return cout << "-1\n", 0;
}
SPFA(1);
cout << (dis[n] >= 2e9? -2 : dis[n]) << '\n';
return 0;
}
Part 5:总结
其实差分约束相对于其他图论问题还是好理解一些的(至少作者是这么认为)。
差分约束的题基本可以分为四个步骤:
- 找出不等关系;
- 不等式推导变形;
- 跑最短路;
- 输出对应的答案(如求出解、求最大值)。
而差分约束的重点就是第一步和第二步,所以大家一定要锻炼自己推导不等式组的能力。
那么就讲到这里了,下课!