「学习笔记」差分约束系统

一.什么是差分约束系统

差分约束系统用来解决形似如下的一组不等式:

\(\begin{cases}x_{1}-x_{2}\leq 0\\ x_{1}-x_{3}\leq 1 \\ x_{2}-x_{3}\leq1\\ x_3-x_4 \leq 3\\x_5 -x_4 \leq 0\\ x_6 - x_7 \leq 2 \end{cases}\)

这一种不等式的特点为:

\(1.\) 两个数的差小于等于某一个常数。

\(2.\) 对于结果,只有两种情况:无解 或 有无数组解。

对于第一个特点,我们同样可以处理大于等于的情况。

处理方法,留在下文讲解。

二.差分约束系统和单源最短路径的关系

在单源最短路径中,任何一条边都满足三角形不等式

即: \(dis_u \leq dis_v + w\)

那我们往最上面的不等式看,也就是:

\(x_i - x_j \leq k\)

我们可以将其变化一下,转化为:

\(x_i \leq x_j + k\)

这样我们就很容易看出,转化后的不等式与单源最短路径中的三角形不等式十分相似

那么这样,就可以使用单源最短路径算法来实现差分约束系统。

三.算法实现步骤

在每一个不等式中,都可以用一个点来表示不等式的未知数。

对于不等式 \(x_i - x_j \leq k\),上文中提到可以转换为\(x_i \leq x_j + k\),也就是说,我们可以在 \(i->j\) 连接一条边权为 \(k\) 的边。

这样就可以很方便的建图,从每个点跑一次单源最短路径,这样就可以得到解,虽然每个点得到的解可能不相同,但是都是正确的。

但是!

我们需要的解肯定是统一的。

那我们该怎么办呢?

我们可以创建一个超级源点\(0\) ,将所有点与其连边。

于是这样,我们就可以从源点 \(0\) 跑一次单源最短路径。

当然,如果要求得到最大解,我们大可以采用单源最长路径。

拓展:

很多情况题目给出的约束条件没有那么方便让我们操作。

有两种可能:

\(1.\) 对于 \(x_i - x_j \geq k\) 的情况,我们可以乘以 \(-1\),将符号颠倒处理即可。

\(2.\) 对于 \(x_i - x_j = k\) 的情况,我们可以将此不等式拆分成 \(2\) 个约束条件:① \(x_i - x_j \leq k\) ②$x_i - x_j \geq k $ 。这样操作就可以了。

四.例题讲解

例题题单: Click Here


例题目录:

\(1.\) P5960 【模板】差分约束算法

\(2.\) P3385 【模板】负环

\(3.\) P1993 小 K 的农场

\(4.\) P3275 [SCOI2011]糖果

\(5.\) P1260 工程规划

\(6.\) P6145 [USACO20FEB]Timeline G


步入正题!

\(1.\) P5960 【模板】差分约束算法

这道题是差分约束系统的模板题。

题意已经很简洁了,不等式也化为了 \(x_i - x_j \leq k\) 的形式。

对于无解的情况,只要一个点入队次数大于 \(n+1\) 次时,那么就出现了负环

对于判断负环,可以先完成此模板题P3385 【模板】负环

出现了负环的情况,那么就是无解,为什么呢?

显然,我们是跑单源最短路径。那么,在负环的情况下,我们就会一直跑一直跑,会达到 \(-\infty\) ,这样自然无解。

然后,那为什么我们要判断入队次数大于 \(n+1\) 次而不是 \(n\) 次而不是别的次数呢?

我们知道,图中原有 \(n\) 个点,如果有解,也就是没有负环的情况,一个点最多只能遍历 \(n\) 个点。

但是为什么是判断入队次数 \(n+1\) 次呢???

不要忘记,在处理差分约束系统时,我们创建了一个超级源点\(0\), 所以,图上的点变成了 \(n+1\) 个。

那么这样,我们只要从超级源点 \(0\) 开始跑单源最短路径,就可以完成这一道模板题了。

Code:

#include <cstdio>
#include <algorithm>
#include <cstring>
#include <iostream>
#include <cmath>
#include <queue>

using namespace std;

const int N = 15555;
const int inf = 0x3f3f3f3f;

struct DAG {
	int nxt;
	int to;
	int w;
}e[N];

int edge_num = 0, head[N], n, m;

inline void add_edge (int x, int y, int z) {
	e[++edge_num].nxt = head[x];
	e[edge_num].to = y;
	e[edge_num].w = z;
	head[x] = edge_num;
	//链式前向星建图 
}

int dis[N], num[N];//num[i]表示点i的入队次数 
	
bool inque[N];//标记该点是否在队列中 

bool SPFA (int x) {
	queue<int> q;
	q.push(x);//先把操作的点入队 
    num[x] ++; //该点入队次数++
	memset (dis, inf, sizeof(dis));//最短路径长度全部设为正无穷 
	dis[x] = 0;//该点的与0的最短路径长度设为0 
	inque[x] = true;//标记该点在队中 
	while (!q.empty()) {//队列非空时进行操作 
		int u=q.front();//取出队头 
		q.pop();//记得弹出该点 
		inque[u] = false;//标记该点已不在队中 
		for (int i = head[u]; i; i = e[i].nxt) {
			int v = e[i].to;
			if (dis[v] > dis[u] + e[i].w) {//三角形不等式 
				dis[v] = dis[u] + e[i].w;//更新 
				if (inque[v] == false) {//不在队列中的情况 
					q.push (v);//将该点入队 
					inque[v] = true;//标记该点在队中 
					num[v] ++;//该点入队次数增加1 
					if (num[v] == n + 1) {//如果该点入队次数达到n+1 
						//那么则出现了负环,返回false 
						return false; 
					}
				}
			}
		}
	}
	return true;//千万不要忘了!!! 
}

int main() {
	scanf ("%d%d", &n, &m);
	for (int i = 1; i <= m; i ++) {
		int u, v, w;
		scanf ("%d%d%d", &u, &v, &w);
		add_edge (v, u, w);
		//我们的不等式是x[u]-x[v]<=w
		//所以连一条v->u边权为w的边权 
	}
	for (int i = 1; i <= n; i++) {
		add_edge (0, i, 0);
		//建超级源点0,所有点与0连接一条边权为0的边 
	}
	if (SPFA(0) == false) {
		puts ("NO");
		//从0开始跑单源最短路径,有负环则输出NO 
	}
	else {
		//无负环输出每个点的最短路径长度 
		for (int i = 1; i <= n; i ++) {
			printf ("%d ", dis[i]); 
		}
	}
	return 0;
}

\(2.\) P3385 【模板】负环

既然在上文的差分约束系统模板中我们提到了负环判断,那么我们就顺手将判断负环模板做了。

按照题意,有的边是建双向边,有的边是建单向边。

我们没有必要建超级源点 \(0\) ,也就是说,在判断负环时,该点入队次数在大于 \(n\) 时就是负环了。

但是

请注意,该题有多组数据,所以在每一测之前都要清空数据。

到此,这题的注意点就没有了,具体实现可看代码。

Code:

#include <cstdio>
#include <algorithm>
#include <iostream>
#include <queue>
#include <cstring>

using namespace std;

const int N = 155555;
const int inf = 0x3f3f3f3f;

struct EDGE {
	int nxt;
	int to;
	int w;
}e[N<<1];

int head[N], edge_num = 0, n, m;

bool inque[N];

int num[N], dis[N];

inline void clear() {//多测的清空操作 
	for (int i = 1; i <= m;i ++) {
		e[i].nxt = 0;
		e[i].to = 0;
		e[i].w = 0;
		edge_num = 0;
		head[i] = 0;
	}
	memset (num, 0, sizeof(num));
	memset (dis, inf, sizeof(dis));
	memset (inque, false, sizeof (false));
}

inline void add_edge (int x, int y, int z) {
	e[++edge_num].nxt = head[x];
	e[edge_num].to = y;
	e[edge_num].w = z;
	head[x] = edge_num;
}

queue<int> q;

void SPFA (int x) {
	q.push (x);//入队 
	dis[x] = 0;
    num[x] ++;//入队次数++
	inque[x] = true;//标记入队 
	while (!q.empty()) {
		int u = q.front();//取队首 
		q.pop();//出队 
		inque[u] = false;//出队后的标记 
		for (int i = head[u]; i; i = e[i].nxt) {
			int v = e[i].to;
			if (dis[v] > dis[u] + e[i].w) {//满足三角形不等式 
				dis[v] = dis[u] + e[i].w;//更新 
				if (inque[v] == false) {//若v点不在队中 
					q.push (v);//将v点入队 
					inque[v] = true;//标记入队 
					num[v] ++;//入队次数加一 
					if (num[v] >= n) {
						//入队次数大于n次时 
						puts("YES");
						return;
					}
				}
			}
		}
	}
	puts("NO");
	return;
}

int main() {
	int T;
	scanf ("%d", &T);
	while (T--) {
		scanf ("%d%d", &n, &m);
		clear();//多测一定要清空 
		for (int i = 1; i <= m; i ++) {
			int x, y, z;
			scanf ("%d%d%d", &x, &y, &z);
			//按照题意建图 
			if (z>=0) {
				add_edge (x, y, z);
				add_edge (y, x, z);
			}
			else {
				add_edge (x, y, z);
			}
		}
		SPFA(1);//没有必要建超级源点,直接从1开始单源最短路径 
	}
	return 0; 
} 

\(3.\) P1993 小 K 的农场

往题目一看,映入眼帘 \(3\) 个条件。

这题的难点再与如何把题目中给出的条件转化为我们建边的操作。

我们可以将这三个条件转化为下面三个(不)等式:

\(1.\) \(x_a - x_b \geq c\)

\(2.\) \(x_a - x_b \leq c\)

\(3.\) \(x_a - x_b = c\)

上文我们说到过,对于 \(x_i - x_j = k\) 的情况,我们可以将此不等式拆分成 \(2\) 个约束条件:① \(x_i - x_j \leq k\) ②$x_i - x_j \geq k $ 。

对于第一个不等式,我们将其乘以 \(-1\) ,变形为:

\(x_b - x_a \leq -c\)

而第二个不等式已经符合我们建图的条件了。

于是,第一个约束条件我们建一条 \(a->b\) 边权为 \(-c\) 的边。

而第二个约束条件,我们建一条 \(b->a\) 边权为 \(c\) 的边。

而这道题更加简单,我们只要从超级源点 \(0\) 开始跑单源最短路径,再判断是否有负环,就可以做出此题了。

Code

#include <cstdio>
#include <algorithm>
#include <cstring>
#include <iostream>
#include <queue>

using namespace std;

const int N = 155555;
const int inf = 0x3f3f3f3f;

struct DAG {
	int nxt;
	int w;
	int to;
}e[N<<1];

int num[N], head[N], edge_num = 0, n, m;

inline void add_edge (int x, int y, int z) {
	e[++edge_num].nxt = head[x];
	e[edge_num].to = y;
	e[edge_num].w = z;
	head[x] = edge_num;
}

bool inque[N];

int dis[N];

inline bool SPFA (int x) {
	queue<int> q;
	q.push(x);//先把操作的点入队 
    num[x] ++;//入队次数++
	memset (dis, inf, sizeof(dis));//最短路径长度全部设为正无穷 
	dis[x] = 0;//该点的与0的最短路径长度设为0 
	inque[x] = true;//标记该点在队中 
	while (!q.empty()) {//队列非空时进行操作 
		int u=q.front();//取出队头 
		q.pop();//记得弹出该点 
		inque[u] = false;//标记该点已不在队中 
		for (int i = head[u]; i; i = e[i].nxt) {
			int v = e[i].to;
			if (dis[v] > dis[u] + e[i].w) {//三角形不等式 
				dis[v] = dis[u] + e[i].w;//更新 
				if (inque[v] == false) {//不在队列中的情况 
					q.push (v);//将该点入队 
					inque[v] = true;//标记该点在队中 
					num[v] ++;//该点入队次数增加1 
					if (num[v] == n + 1) {//如果该点入队次数达到n+1 
						//那么则出现了负环,返回false 
						return false; 
					}
				}
			}
		}
	}
	return true;//千万不要忘了!!! 
}

int main() {
	scanf ("%d%d", &n, &m);
	for (int i = 1; i <= m; i ++) {
		int op, a, b, c;
		scanf ("%d%d%d", &op, &a, &b);
		if (op == 1) {
			scanf ("%d", &c);
			add_edge (b, a, -c);
		}
		if (op == 2) {
			scanf ("%d", &c);
			add_edge (a, b, c);
		}
		if (op == 3) {
			add_edge (a, b, c);
			add_edge (b, a, c);
		}
	} 
	for (int i = 1; i <= n; i ++) {
		//所有点与超级源点0连边 
		add_edge (0, i, 0);
	}
	if (SPFA(0) == false) {
		//有负环无解 
		puts("No");
	}
	else {
		puts("Yes");
	}
	return 0;
}

\(4.\) P3275 [SCOI2011]糖果

在上文中我们提到,当不等式形式为 \(x_i - x_j \geq k\) 时,我们将其乘以 \(-1\),使其符号颠倒。

同样的,我们还有一种处理方法。

对于\(x_i - x_j \geq k\) ,我们同样可以建一条 \(j->i\) 边权为 \(k\) 的边,但是我们要跑最长路径

更改仅仅在于三角形不等式时,将大于号改为小于号即可。

即: \(dis_u < dis_v + w\)

这题的难点依旧在于建边。

看到题目,给出了五个约束条件。

我们先分析,粗略地列出下面的不等式组:

\(\begin{cases}1\cdot A=B\\ 2\cdot A <B\\ 3\cdot A\geq B\\ 4\cdot A >B\\ 5\cdot A\leq B\end{cases}\)

然后我们将其一个个转化为 \(x_i - x_j \geq k\) 的形式。

\(trick\):当出现 \(>\)\(<\) 的情况,我们可以通过 \(+1\)\(-1\) 来使符号变为 \(\leq\)\(\geq\)

对于不等式 \(1\):上文提过,转化为 \(A-B\leq 0\)\(A-B \geq 0\)

对于不等式 \(2\):可以用到上文的小 \(trick\) ,先化为 \(A+1\leq B\) 的形式,然后移项,再乘以 \(-1\),可转化为 \(B-A \geq 1\)

对于不等式 \(3\):我们直接移项,化为 \(A-B \geq 0\)

对于不等式 \(4\):我们也可以用到上文中的小 \(trick\),先化为\(A\geq B+1\),然后移项,变为 \(A-B \geq 1\)

对于不等式 \(5\):先移项化为 \(A-B\leq 0\) ,然后乘以 \(-1\),化为 \(B-A \geq 0\)

这样,我们的 \(5\) 个约束条件就转换完成了。

我们就可以按照上文说过的建图方式建图。

由于题目要求是求至少要准备的糖果数,所以我们要将跑的最长路径的值加起来。

注意! 我们要跑最长路径

但是有个很奇怪的事情,也许是因为数据的特殊性,我用 \(queue\) 死活会挂一个点,用 \(stack\) 就可以过。

Code:

#include<cstdio>
#include<cctype>
#include<algorithm>
#include<cstring>
#include<queue>
#include<stack>
#include<iostream>
#define R register

using namespace std;

typedef long long ll;

#define getchar()(p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
char buf[1<<21],*p1=buf,*p2=buf;
inline int read() {
    R char c=getchar();R int x=0;R bool f=0;
    for(;!isdigit(c);c=getchar())f^=!(c^45);
    for(;isdigit(c);c=getchar())x=(x<<1)+(x<<3)+(c&15);
    return f?(~x+1):x;
}

const int N = 150005;

struct DAG {
	int nxt;
	int to;
	int w;
}G[N];

int n, m;

int outdeg[N], head[N], edge_num = 0, dis[N];

bool vis[N];

inline void add_edge (int x, int y, int z) {
	G[++edge_num].nxt = head[x];
	G[edge_num].to = y;
	G[edge_num].w = z;
	head[x] = edge_num;
}

bool tag = true;

inline void SPFA () {
	stack<int> st;
	st.push(0);
	while (!st.empty()) {
		int u = st.top();
		outdeg[u] ++;
		if (outdeg[u] == n + 1) {
        //出现负环
			printf("-1");
			tag = false;
			return ;
		}
		st.pop();
		vis[u] = false;
		for (R int i = head[u]; i; i = G[i].nxt) {
			int v = G[i].to;
			if (dis[v] < dis[u] + G[i].w) {
            //三角形不等式
				dis[v] = dis[u] + G[i].w;
				if (vis[v] == false) {
					vis[v] = true;
					st.push (v);
				}
			}
		}
	}
}

int main() {
	n = read(), m = read();
	while (m--) {
		int op = read(), u = read(), v = read();
		if (op == 1) {
			add_edge (u, v, 0);
			add_edge (v, u, 0);
		}
		if (op == 2) {
			if (u == v) {
				puts ("-1");
				return 0;
			}
			add_edge (u, v, 1);
		}
		if (op == 3) {
			add_edge (v, u, 0);
		}
		if (op == 4) {
			if (u == v) {
				puts ("-1");
				return 0;
			}
			add_edge (v, u, 1);
		}
		if (op == 5) {
			add_edge (u, v, 0);
		}
	}	
	for (int i = 1; i <= n; i ++) {
		add_edge (0, i, 1);
        //与超级源点0连边
	}
	vis[0] = true;
	SPFA ();
	if (tag == false) {
		return 0;
	}
	ll sum = 0;
	for (int i = 1; i <= n; i ++) {
		sum += dis[i];
	}
	printf ("%lld", sum);
	return 0;
}

\(5.\) P1260 工程规划

仔细阅读题目,我们就能发现,题目又给出了 \(m\)\(T_i - T_j \leq b\) 的不等式。

这样我们按照差分约束的基本套路,建一条 \(j->i\) 边权为 \(b\) 的边即可。

但是题目要求每个任务的起始时间,按照题意,我们知道,我们跑完单源最短路径后,最小的 \(dis\) 就是该工程的最早开始时间。

于是,我们最后需要找到最小的 \(dis\) ,最后输出每一个 \(dis\) 与最小的 \(dis\) 的差值。

Code:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <iostream>
#include <queue>

using namespace std;

typedef long long ll;

const int N = 15555;
const int inf = 0x3f3f3f3f;

struct EDGE {
	int to;
	int nxt;
	int w;
}e[N<<1];

int edge_num = 0, head[N];

inline void add_edge (int x, int y, int z) {
	e[++edge_num].nxt = head[x];
	e[edge_num].to = y;
	e[edge_num].w = z;
	head[x] = edge_num;
}

int dis[N], num[N], n, m;

bool inque[N], tag = false;
//inque标记是否在队中
//tag标记是否有负环 

inline void SPFA(int x) {
	queue<int> q;
	memset (dis, inf, sizeof (dis));//边权最初最大化 
	q.push (x);//入队 
	num[x] ++;//入队次数加一 
	dis[x] = 0;//源点与源点的最短路径长度为0 
	inque[x] = true;//标记在队中 
	while (!q.empty()) {//队列非空循环 
		int u = q.front();//取队头 
		q.pop();//弹出队头 
		inque[u] = false;//标记不在队中 
		for (int i = head[u]; i; i = e[i].nxt) {
			int v = e[i].to;
			if (dis[v] > dis[u] + e[i].w) {//三角形不等式 
				dis[v] = dis[u] + e[i].w;//更新边权 
				if (inque[v] == false) {//未入队标记 
					q.push (v);//入队 
					inque[v] = true;//标记入队 
					num[v] ++; //入队次数加一 
					if (num[v] >= n + 1) {//入队次数超过点数,则出现负环 
						tag = true;//标记出现负环 
						return ;//退出循环 
					}
				} 
			}
		}
	}
	return ;
}

int main() {
	scanf ("%d%d", &n, &m);
	for (int i = 1; i <= m; i ++) {
		int x, y, z;
		scanf ("%d%d%d", &x, &y, &z);
		add_edge (y, x, z);
		//tx-ty≤z 所以建一条y->x 边权为z的边 
	} 
	for (int i = 1; i <= n; i ++) {
		add_edge (0, i, 0);
		//与超级源点0建一条边权为0的边 
	}
	SPFA (0);//从超级源点0开始跑单源最短路径 
	if (tag == true) {
		puts ("NO SOLUTION");
		//出现负环无解 
		return 0;
	}
	int min_dis = inf;
	for (int i = 1; i <= n; i ++) {
		min_dis = min (min_dis, dis[i]);
		//寻找最小的dis 
	}
	for (int i = 1; i <= n; i ++) {
		printf ("%d\n", dis[i] - min_dis);
		//输出每个dis与最小dis的差值 
	}
	return 0;
}

\(6.\) P6145 [USACO20FEB]Timeline G

题目给出了两个条件:

\(I\): 第 \(b\) 次挤奶在第 \(a\) 次挤奶结束至少 \(x\) 天后进行。

\(II\):第 \(i\) 次挤奶不早于第 \(S_i\) 天。

这样,我们就能转化出两个不等式:

\(b-a\geq x\)

\(i\geq S_i\) ,我们也可以将其化为 \(i - 0 \geq S_i\)

于是,我们就拥有了两个约束条件。

建两种边:

\(1.\)\(a->b\) 边权为 \(x\) 的边。

\(2.\)\(0->i\) 边权为 \(S_i\) 的边。

因为约束条件是 \(\geq\) 形式的,所以跑单源最长路径。

Code:

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>

using namespace std;

typedef long long ll;

const int N = 3e5 + 7;
const int inf = 0x3f3f3f3f;

struct EDGE {
	int nxt;
	int to;
	int w;
}e[N];

int head[N], edge_num = 0, n, m, c;

inline void add_edge (int x, int y, int z) {
	e[++edge_num].nxt = head[x];
	e[edge_num].to = y;
	e[edge_num].w = z;
	head[x] = edge_num;
}

ll dis[N];

bool inque[N];

inline void SPFA (int s) {
	queue<int> q;
	memset (dis, -inf ,sizeof (dis));//最长路,边权初始为负无穷 
	dis[s] = 0;//源点与源点的最短路径长度为0 
	q.push(s); //源点入队 
	inque[s] = true;//标记入队 
	while (!q.empty()) {
		int u = q.front();//取出队头 
		q.pop();//弹出队头 
		inque[u] = false;//标记不在队中 
		for (int i = head[u]; i; i = e[i].nxt) {
			int v = e[i].to;
			if (dis[v] < dis[u] + e[i].w) {//三角形不等式 
				dis[v] = dis[u] + e[i].w;//更新 
				if (inque[v] == false) {//不在队中时 
					q.push (v);//入队 
					inque[v] = true;//标记入队 
				}
			}
		}
	}
}

int main() {
	scanf ("%d%d%d", &n, &m, &c);
	for (int i = 1; i <= n; i ++) {
		int t;
		scanf ("%d", &t);
		add_edge(0, i, t);//超级源点连边 
	}
	for (int i = 1; i <= c; i ++) {
		int x, y, z;
		scanf ("%d%d%d", &x, &y, &z);
		add_edge (x, y, z);
	}
	SPFA(0);//从超级源点开始跑单源最长路径 
	for (int i = 1; i <= n; i ++) {
		printf ("%lld\n", dis[i]);
	}
	return 0;
} 
posted @ 2021-07-21 10:23  cyhyyds  阅读(150)  评论(0编辑  收藏  举报