差分约束系统小结
一、什么是差分约束系统?#
1. 引例#
给定个变量和个不等式,每个不等式形如$(0 \le i, j < n, 0 \le k < m, a_k)x_{n-1} - x_0n = 4m = 5x_3-x_0$的最大值。
我们考虑暴力。
- :
- :
- :
所以有,即的最小值为。
当然,对于更大的数据,我们希望能用算法系统地处理,想一想,我们暴力计算的过程跟什么有点像?
看图,如果让我们求这张图的最短路,我们也许很快就能敲完一份完美的代码。
再仔细看看这张图,你会发现,这张图似乎与前面的不等式限制条件一一对应。
没错,我们需要用图论的知识把不等式关系转换成图中点与边之间的关系。
如若一个系统由个变量和个不等式组成,并且这个不等式对应的系数矩阵中每一行有且仅有一个和,其它的都为,这样的系统称为差分约束( difference constraints )系统。前面的不等式组同样可以表示成如图的系数矩阵。(摘自夜深人静写算法系列)
现在,我们思考如何转换。
二、代数与图形的关系及原理#
1.原理的感性理解#
对于一个单独的不等式来说,将这个等式移项,可得,这与单源最短路问题中的很像啊。
也就是说,在求最短路时,如果出现,我们就会更新,力求,这不与我们求解不等式的解如出同辙吗?
也就是说,形如的式子,我们连一条由到,权值为的有向边,然后在图上跑一遍最短路,就能得到我们想要的答案。
2. 三角不等式#
我们来看一个简化的情况。
我们同样想知道的最大值,,可得,再有个,所以实质上我们在求,把这个建个图你就会发现,正好对应了到的最短路。
3. 解的有无#
因为我们已成功地将不等式关系转化成了图中的关系,所以当图异常结束的时候,就说明原不等式组无解。这里的异常结束包括最短路中的负权环,最长路中的正权环。
当然,若未更新我们想知道的点的值,就说明它未在差分约束系统里,所有取值都是可行的
只要最短(长)路算法正常结束并且所有变量都没有确定的值,那么该不等式必然有无限多组解,并且当前所求出的一组解必然是满足题目条件的边界解;一般情况下,我们会新建一个节点值为,与其他点相连,有时题目会给出某些初始点的值。
4. 最大值与最小值的转化#
前面我们有意无意提到了最长路,也就是说,我把前面不等式反号,把变成,照样可以建图,然后跑最长路,思想与前面一样。
当然,也可以通过数学方法,两边乘个,变成前面的最短路处理。
三、差分约束的应用&题目#
1. 线性约束:布局Layout#
大意:#
一维线上有个点,有些点之间距离不能大于某个数,也有些点之间距离不能小于某个数,求第个点到第个点的距离最小是多少,若没有合法情况,输出,若可以取无限大,输出。
分析:#
表示号位置到的距离,那么我们可以知道,单调不下降。
所以有第一类限制条件:。
同时,它给出了对,对,直接连边。这里的为0,求即为最终答案。
有个坑点,首先应先判断整张图有没有环,即新建节点连向所有节点,这样也不会对后面计算答案产生影响并且可以遍历完整张图。
然后再从节点跑,若未被更新,就说明最短路无限长,否则输出最短路的长度。
代码:#
#include <queue>
#include <vector>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define Re register
using namespace std;
const int MAX = 1e4 + 5;
const int INF = 0x7f7f7f7f;
const int RS = -2139062144;
inline int read(){
int f = 1, x = 0; char ch;
do { ch = getchar(); if (ch == '-') f = -1; } while (ch < '0' || ch > '9');
do {x = (x << 3) + (x << 1) + ch - '0'; ch = getchar(); } while (ch >= '0' && ch <= '9');
return f * x;
}
struct Sakura { int to, nxt, w; }sak[MAX << 1]; int head[MAX], cnt;
inline void add(int x, int y, int w) {
++cnt;
sak[cnt].to = y, sak[cnt].w = w, sak[cnt].nxt = head[x], head[x] = cnt;
}
int n, ml, md;
struct SPFA {
int dis[MAX], cnt[MAX], vis[MAX];
inline bool Run(int st) {
for (int i = 0;i <= n; ++i) dis[i] = 1e9, cnt[i] = 0, vis[i] = 0;
queue <int> Q;
dis[st] = 0;
vis[st] = 1;
Q.push(st);
while (!Q.empty()) {
int u = Q.front();
Q.pop();
vis[u] = 0;
++cnt[u];
if (cnt[u] >= n) {
puts("-1");
exit(0);
}
for (int i = head[u];i;i = sak[i].nxt) {
int v = sak[i].to, w = sak[i].w;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
if (!vis[v]) {
Q.push(v);
vis[v] = 1;
}
}
}
}
return 1;
}
}Spfa;
int main(){
n = read(), ml = read(), md = read();
for (int i = 1;i <= ml; ++i) {
int a = read(), b = read(), d = read();
add(a, b, d);
}
for (int i = 1;i <= md; ++i) {
int a = read(), b = read(), d = read();
add(b, a, -d);
}
for (int i = 2;i <= n; ++i) {
add(i, i - 1, 0);
}
for (int i = 1;i <= n; ++i) {
add(0, i, 0);
}
Spfa.Run(0);
Spfa.Run(1);
if (Spfa.dis[n] == 1e9) {
printf("-2");
return 0;
}
else printf("%d", Spfa.dis[n]);
return 0;
}
2. 区间约束:Intervals #
大意:#
给定个区间约束条件,要求中至少有个数,求构造出的总个数最少。
分析:#
区间上,设表示区间为上的个数为多少,即有:,且。
考虑前缀和,所以就有:,。
然后就可以做了。
#include <queue>
#include <vector>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define Re register
using namespace std;
const int MAX = 200000 + 5;
const int INF = 0x7f7f7f7f;
inline int read(){
int f = 1, x = 0; char ch;
do { ch = getchar(); if (ch == '-') f = -1; } while (ch < '0' || ch > '9');
do {x = (x << 3) + (x << 1) + ch - '0'; ch = getchar(); } while (ch >= '0' && ch <= '9');
return f * x;
}
struct Sakura { int to, nxt, w; }sak[MAX << 1]; int head[MAX], cnt;
inline void add(int x, int y, int w) {
++cnt;
sak[cnt].to = y, sak[cnt].w = w, sak[cnt].nxt = head[x], head[x] = cnt;
}
int t, n, maxx, minn = INF;
struct SPFA {
int dis[MAX];
int vis[MAX]; queue <int> Q;
inline void Run(int st) {
for (int i = 0;i <= maxx; ++i) dis[i] = -INF;
memset(vis, 0, sizeof vis);
dis[st] = 0;
vis[st] = 1;
Q.push(st);
while (!Q.empty()) {
int u = Q.front();
Q.pop();
vis[u] = 0;
for (int i = head[u];i;i = sak[i].nxt) {
int v = sak[i].to, w = sak[i].w;
if (dis[v] < dis[u] + w) {
dis[v] = dis[u] + w;
if (!vis[v]) {
Q.push(v);
vis[v] = 1;
}
}
}
}
}
}Spfa;
int main(){
t = read();
while (t --) {
n = read();
memset(head, 0, sizeof head);
cnt = 0;
for (int i = 1;i <= n; ++i) {
int a = read() + 1, b = read() + 1, w = read();
minn = min(minn, a - 1);
maxx = max(maxx, b);
add(a - 1, b, w);
}
for (int i = 1;i <= maxx; ++i) add(i - 1, i, 0), add(i, i - 1, -1);
Spfa.Run(0);
printf("%d\n", Spfa.dis[maxx]);
if (t != 0) puts("");
}
return 0;
}
3. 未知约束:出纳员问题#
大意:略#
分析:#
限制条件有点多,慢慢来整理一下。
表示在时刻最多能招到多少人,表示在时刻至少需要多少人。
首先,设表示在时刻雇佣的人数,则有。
其次,有,还有。
设为的前缀和,所以就有:
前两个好办,关键是第三个。
看到我把移到了右边,也许你会猜到什么了吧?
没错,枚举,从到枚举,看是否出现可行解,当然记得要确保图中的节点的值就是你枚举的值,我们可以新建一个值为的节点,就有你枚举的值,把它拆成两个不等式,搞定。
然后,思考,当招个人能满足要求时,个人也能满足要求,直接二分求解。
#include <queue>
#include <vector>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define Re register
using namespace std;
const int MAX = 200000 + 5;
const int INF = 0x7f7f7f7f;
inline int read(){
int f = 1, x = 0; char ch;
do { ch = getchar(); if (ch == '-') f = -1; } while (ch < '0' || ch > '9');
do {x = (x << 3) + (x << 1) + ch - '0'; ch = getchar(); } while (ch >= '0' && ch <= '9');
return f * x;
}
struct Sakura { int to, nxt, w; }sak[MAX << 1]; int head[MAX], cnt;
inline void add(int x, int y, int w) {
++cnt;
sak[cnt].to = y, sak[cnt].w = w, sak[cnt].nxt = head[x], head[x] = cnt;
}
int T, R[MAX], num[MAX], n;
struct SPFA {
int dis[MAX], cnt[MAX], vis[MAX];
inline bool Run(int st) {
memset(dis, 128, sizeof dis);
memset(cnt, 0, sizeof cnt);
memset(vis, 0, sizeof vis);
queue <int> Q;
dis[st] = 0;
vis[st] = 1;
Q.push(st);
while (!Q.empty()) {
int u = Q.front();
Q.pop();
vis[u] = 0;
for (int i = head[u];i;i = sak[i].nxt) {
int v = sak[i].to, w = sak[i].w;
if (dis[v] < dis[u] + w) {
dis[v] = dis[u] + w;
if (!vis[v]) {
Q.push(v);
vis[v] = 1;
if (++cnt[v] > n) return 0;
}
}
}
}
return 1;
}
}Spfa;
int main(){
T = read();
while (T --) {
bool can = 0;
for (int i = 1;i <= 24; ++i) {
R[i] = read();
if (R[i]) can = 1;
}
for (int i = 0;i <= 24; ++i) num[i] = 0;
n = read();
for (int i = 1;i <= n; ++i) {
int x = read() + 1;
num[x] ++;
}
if (!can) {
printf("0\n");
continue;
}
bool flag = 0;
int l = 0, r = n;
while (l < r) {
int mid = (l + r) >> 1;
memset(head, 0, sizeof head);
cnt = 0;
for (int i = 9;i <= 24; ++i) add(i - 8, i, R[i]);
for (int i = 1;i <= 8; ++i) add(i + 16, i, R[i] - mid);
for (int i = 1;i <= 24; ++i) add(i, i - 1, -num[i]), add(i - 1, i, 0);
add(0, 24, mid);
add(24, 0, -mid);
if (Spfa.Run(0)) r = mid, flag = 1;
else l = mid + 1;
}
if (flag) printf("%d\n", r);
else {
memset(head, 0, sizeof head);
cnt = 0;
for (int i = 9;i <= 24; ++i) add(i - 8, i, R[i]);
for (int i = 1;i <= 8; ++i) add(i + 16, i, R[i] - r);
for (int i = 1;i <= 24; ++i) add(i, i - 1, -num[i]), add(i - 1, i, 0);
add(0, 24, r);
add(24, 0, -r);
if (Spfa.Run(0)) printf("%d\n", r);
else printf("No Solution\n");
}
}
return 0;
}
4. 合理转化:糖果 #
大意:#
略略略~
分析:#
把前面弄懂就会做了。
代码:#
#include<cstdio>
#include<cstdlib>
#include<cstring>
#define ll long long
#define Re register
#define Min(a, b) ((a) < (b) ? (a) : (b))
const int MAX = 500000 + 5;
inline int read(){
int f = 1, x = 0;char ch;
do { ch = getchar(); if (ch == '-') f = -1; } while (ch < '0'||ch>'9');
do {x = x*10+ch-'0'; ch = getchar(); } while (ch >= '0' && ch <= '9');
return f*x;
}
struct sakura {
int to, nxt, w;
}sak[MAX]; int head[MAX], cnt;
inline void add(int x, int y, int w) {
++cnt;
sak[cnt].to = y, sak[cnt].nxt = head[x], sak[cnt].w = w, head[x] = cnt;
}
ll ans;
int n, k, q[MAX], h, t = 1, dis[MAX], vis[MAX], cnts[MAX];
inline bool SPFA(int st) {
q[1] = st;
dis[st] = 0;
vis[st] = 1;
while (h < t) {
int u = q[++h];
cnts[u]++;
if (cnts[u] > n - 1) return 0;
vis[u] = 0;
for (int i = head[u];i;i = sak[i].nxt) {
int v = sak[i].to, w = sak[i].w;
if (dis[v] < dis[u] + w) {
dis[v] = dis[u] + w;
if (!vis[v]) q[++t] = v, vis[v] = 1;
}
}
}
return 1;
}
int main(){
n = read(), k = read();
for (int i = 1;i <= k; ++i) {
int x = read(), a = read(), b = read();
switch(x) {
case 1:{
add(a, b, 0);
add(b, a, 0);
break;
}
case 2:{
if (a == b) {
puts("-1");
return 0;
}
add(a, b, 1);
break;
}
case 3:{
add(b, a, 0);
break;
}
case 4:{
if (a == b) {
puts("-1");
return 0;
}
add(b, a, 1);
break;
}
case 5:{
add(a, b, 0);
break;
}
}
}
for (int i = n;i >= 1; --i) {
add(n + 1, i, 1);
}
if (SPFA(n + 1)) {
for (int i = 1;i <= n; ++i) {
ans += dis[i];
}
printf("%lld", ans);
}
else printf("-1");
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!