差分约束
什么是差分约束系统?
差分约束系统是一种特殊的\(N\)元一次不等式组,它包含\(N\)个变量以及\(M\)个约束条件,每个约束条件都是由两个变量作差得到的,形如\(X_i - X_j ≤ C_k\),其中\(C_k\)是常数。
我们根据题目要求,并用这\(M\)个约束条件求出某个不等式的最值,例如\(X_N - X_1\)的最大值。
怎么解?
-
转化:
把上面\(X_i - X_j ≤ C_k\)不等式稍微变形一下可以得到\(X_i ≤ C_k + X_j\),令\(X_i = dis_i\),\(X_j = dis_j\),\(C_k = edge_{i, j}\),得到\(dis_i ≤ edge_{i, j} + dis_j\),是不是联想到了最短路算法?
因此我们可以把这\(M\)个不等式转化到图中,例如对于\(X_i ≤ C_k + X_j\),则在图中连一条从\(j\)到\(i\)的有向边,边权为\(C_k\)。
这时候假如题目需要我们求\(X_N - X_1\)的最大值,我们便从图中求出\(1\)到\(N\)的最短路即可。
为什么是最短路?由于有约束条件,图中可能有更长的路的存在,但是如果选了更长的路,那么就不满足最短路那一条路的约束条件了,即最短路是所有满足条件的路中最长的一条。
-
解的存在性:
我们知道不等式的解有三种情况:有解、无解、无限个解。
这三种情况对应图中的情况分别是什么样的呢?
有解:在图中可以找到最短路。
无解:图中存在负环,没有最短路。
无限个解:图中我们要求的两点间无法到达,即不连通。
-
最小值:
如果题目给出的条件形如\(X_i - X_j ≥ C_k\),最后要我们求出\(X_N - X_1\)的最小值,显然求出\(1\)到\(N\)的最长路即可。
-
不等式标准化:
如果题目给出的约束条件既有"\(≥\)"也有"\(≤\)"该怎么办?
根据题目要求,如果需要求最大值,那么把不等式都转化为"\(≤\)",否则转化为"\(≥\)"的形式。
但是要是题目给出了等式该怎么办?
例如\(X_i - X_j = C_k\),那么我们可将其转化成两个不等式:\(X_i - X_j ≥ C_k\)和\(X_i - X_j ≤ C_k\)。
试题
T1:种树
T2:小K的农场
题解
种树:
不妨设\(sum_i\)为从\(1\)到\(i\)中种树的数量,根据题意,可以列出以下不等式:
\(sum_i - sum_{j - 1} ≥ c\)(区间\(j\)到\(i\)至少要种\(c\)课树)
\(0 ≤ sum_i - sum_{i - 1} ≤ 1\) (每块地区最多种一棵树)
题目需要我们求最小值,那么我们把上面两个不等式都换成"\(≥\)"的形式:
\(sum_i - sum_{j - 1} ≥ c\)
\(sum_{i - 1} - sum_i ≥ -1\)
\(sum_i - sum_{i - 1} ≥ 0\)
再稍微变形一下:
\(sum_i ≥ c + sum_{j - 1}\)
\(sum_{i - 1} ≥ -1 + sum_i\)
\(sum_i ≥ 0 + sum_{i - 1}\)
这时候应该很清楚了吧:
在图中连一条从\(j - 1\)到\(i\)的有向边,边权为\(c\);
在图中连一条从\(i\)到\(i - 1\)的有向边,边权为\(-1\);
在图中连一条从\(i - 1\)到\(i\)的有向边,边权为\(0\)。
最后用SPFA跑一遍最长路,答案就是\(sum_N\)。
代码:
#include <iostream>
#include <cstdio>
#include <queue>
using namespace std;
const int N = 30010;
int n, h, tot, head[N], ver[N << 2], nex[N << 2], edge[N << 2], sum[N], vis[N];
inline void add (int x, int y, int z) {
ver[++ tot] = y;
edge[tot] = z;
nex[tot] = head[x];
head[x] = tot;
}
void spfa (int s) {
for (int i = 1; i <= n; i ++)
sum[i] = -0x3f3f3f3f;
queue <int> q;
q.push(s);
sum[s] = 0;
while (!q.empty()) {
int x = q.front();
q.pop();
vis[x] = 0;
for (int i = head[x]; i; i = nex[i]) {
int y = ver[i];
if (sum[y] < sum[x] + edge[i]) {
sum[y] = sum[x] + edge[i];
if (!vis[y]) {
q.push(y);
vis[y] = 1;
}
}
}
}
}
int main () {
scanf("%d%d", &n, &h);
for (int i = 1; i <= h; i ++) {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
add(x - 1, y, z);
}
for (int i = 0; i <= n; i ++) {
if (i)
add(i - 1, i, 0);
if (i != n)
add(i, i - 1, -1);
}
spfa(0);
printf("%d", sum[n]);
return 0;
}
小K的农场:
还是先根据题目,列出所有约束条件:
\(a - b ≥ c\)
\(a - b ≤ c\)
\(a = b\)
题目没有直接说要求最大值还是最小值,只是让我们判断是否有解。
很容易可以想到只有当出现负环时无解,而判断负环需要用SPFA跑最短路,所以我们要求出最大值,因此把所有约束条件换成"\(≤\)"的形式:
\(b - a ≤ -c\)
\(a - b ≤ c\)
\(a - b ≤ 0\)
\(b - a ≤ 0\)
稍微变形得:
\(b ≤ -c + a\)
\(a ≤ c + b\)
\(a ≤ 0 + b\)
\(b ≤ 0 + a\)
对应图中的操作:
在图中连一条从\(a\)到\(b\)的有向边,边权为\(-c\);
在图中连一条从\(b\)到\(a\)的有向边,边权为\(c\);
在图中连一条\(a\)和\(b\)之间的无向边,边权为\(0\)。
最后用SPFA跑最短路并判断负环就可以啦!
注意!
这题BFS版的SPFA可能会被卡,用DFS版的SPFA就快了很多。
还有以上方法连成的图不一定是连通的,需要以每个节点为起点分别跑一次SPFA(可以用\(vis_i\)标记\(i\)是否被访问过,如果被访问了就可以不用重新跑一遍了!)
我试过新建一个起点连接所有节点,然后从这个起点开始只跑一次SPFA,但是不知道为什么会WA!qwq
代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int N = 10010, M = 100010;
int t, n, m, tot, head[N], ver[M << 1], nex[M << 1], edge[M << 1], dis[N], vis[N], flag;
inline void add (int x, int y, int z) {
ver[++ tot] = y;
edge[tot] = z;
nex[tot] = head[x];
head[x] = tot;
}
bool spfa (int x) {
vis[x] = 1;
for (int i = head[x]; i; i = nex[i]) {
int y = ver[i], w = edge[i];
if(dis[y] > dis[x] + w) {
if (vis[y]) return false;
dis[y] = dis[x] + w;
if (!spfa(y)) return false;
}
}
vis[x] = 0;
return true;
}
int main () {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i ++) {
int opt, x, y, z;
scanf("%d", &opt);
if (opt == 1) {
scanf("%d%d%d", &x, &y, &z);
add(x, y, -z);
} else if (opt == 2) {
scanf("%d%d%d", &x, &y, &z);
add(y, x, z);
} else {
scanf("%d%d", &x, &y);
add(x, y, 0);
add(y, x, 0);
}
}
memset(dis, -0x3f, sizeof(dis));
for (int i = 1; i <= n; i ++)
if (!vis[i]) {
dis[i] = 0;
if (!spfa(i)) {
printf("No");
return 0;
}
}
printf("Yes");
return 0;
}

浙公网安备 33010602011771号