一、定义

现有 \(n\) 个变量 \(a_1, a_2, ... ,a_n\),给出 \(m\) 个不等式,第 \(i\) 个形如 \(a_i - a_j \leq k\)\(k\) 为常数, \(1 \leq i, j \leq m\) ),请构造出一组符合条件的数列 \(a\)

二、解法

要求构造 \(a\),直接暴力不好搞,时间复杂度也很高,考虑转化问题。

\(dis_i\) 为超级原点到节点 \(i\) 的最短路径长,如果一个节点 \(u\) 可以通过一条权值为 \(w\) 的边到达节点 \(v\) 的话,根据最短路径的性质,则一定满足 \(dis_v \leq dis_u + w\)。化简可得 \(dis_v - dis_u \leq w\)。与上述差分约束的不等式及其相似。所以当我们拿到一个不等式 \(a_i - a_j \leq k\) 时,可以在节点 \(j\) 和节点 \(i\) 间连接一条权值为 \(k\) 的有向边,再建立一个超级源点,求出到任意节点 \(i\) 的最短路径,便得到了一组答案。

当然,有时候题目中有隐藏的限制条件,这就因题而异了。

什么?你问怎么考虑无解的情况?还有无解的情况?!

是的,怎么考虑无解呢?

既然我们已经将问题转化成求解最短路了,所以只需要搞清楚最短路什么时候无解就行了。最短路无解的情况只有一种:图中存在 负环

因此,Dijkstra 死掉了。所以处理差分约束类问题时,我们一般选择 SPFA 算法。设节点 \(i\) 入队次数为 \(num_i\),则当 \(num_i \geq n\) 时,图中存在负环。

补充:常见的不等式转化方式如下:

\(a - b = k\),转化为 \(a - b \leq k\)\(a - b \geq k\)

\(a - b > k\),转化为 \(a - b \geq k + 1\)

\(a - b < k\),转为为 \(a - b \leq k - 1\)

三、例题

1、差分约束系统

分析:

裸题,直接套用上述结论即可。

顺便挂个模板:

#include<bits/stdc++.h>
using namespace std;
#define SF scanf
#define PF printf
struct Edge {
	int to, next, w;
}edge[50005];
struct node {
	int dis, id;
};
int n, m, cnt, head[5005], num[5005], dis[5005];
bool vis[5005];
void add(int u, int v, int w) {
	edge[++cnt].to = v;
	edge[cnt].next = head[u];
	edge[cnt].w = w;
	head[u] = cnt;
}
void SPFA(int x) {
	for(int i = 1; i <= n; i++) dis[i] = 0x3f3f3f3f;
	dis[x] = 0;
	queue<node> q;
	q.push((node){0, x}), vis[x] = 1;
	while(!q.empty()) {
		node tmp = q.front();
		q.pop();
		vis[tmp.id] = 0;
		num[tmp.id]++;
		if(num[tmp.id] == n) {
			PF("NO");
			exit(0);
		}
		for(int i = head[tmp.id]; i; i = edge[i].next) {
			int to = edge[i].to;
			if(dis[to] > dis[tmp.id] + edge[i].w) {
				dis[to] = dis[tmp.id] + edge[i].w;
				if(!vis[to]) {
					q.push((node){dis[to], to});
					vis[to] = 1;
				}
			}
		}
	}
}
int main() {
	SF("%d%d", &n, &m);
	for(int i = 1; i <= n; i++) add(0, i, 0);
	for(int i = 1; i <= m; i++) {
		int u, v, w;
		SF("%d%d%d", &u, &v, &w);
		add(v, u, w);
	}
	SPFA(0);
	for(int i = 1; i <= n; i++) PF("%d ", dis[i]);
	return 0;
}

2、天平

分析:

显然输入给了我们很多不等式。

一共有 \(n\) 个砝码,设第 \(i\) 个砝码重 \(w_i\)\(1 \leq w_i \leq 3\) )克。

现在已经给出了两个砝码 \(A\)\(B\)。把它们放在了天平的一边,还需要另外两个砝码 \(C\)\(D\),放在天平的另一边。

因为 \(4 \leq n \leq 50\),范围很小,可以直接对 \(C\)\(D\)进行枚举。

要分别求出 \(w_A + w_B > w_C + w_D\)\(w_A + w_B = w_C + w_D\)\(w_A + w_B < w_C + w_D\)的方案数,设其分别为 \(c1\)\(c2\)\(c3\)

关键在于怎样求出每一个 \(w_i\),但这明显有点难,而且答案可能不唯一。

考虑对要求的三个不等式进行转化。

若要满足 \(w_A + w_B > w_C + w_D\),即满足 \(w_A - w_C > w_D - w_B\)\(w_A - w_D > w_C - w_B\) 即可。

如果 \(\min\{w_A - w_C\} > \max\{w_D - w_B\}\)\(\min\{w_A - w_D\} > \max\{w_C - w_B\}\),则一定满足第一个不等式。累计 \(c1\)

其余两个不等式转化方式同上。

至此,问题转化为:对于任意的 \(i\)\(j\),设其满足条件的最小差值为 \(minn_{i,j}\),满足条件的最大差值为 \(maxn_{i,j}\),求解出所有的 \(minn\)\(maxn\) 即可。

Code:

#include<bits/stdc++.h>
using namespace std;
#define SF scanf
#define PF printf
int Min[55][55], Max[55][55], n;
void floyd() {
	for(int k = 1; k <= n; k++) {
		for(int i = 1; i <= n; i++) {
			if(i == k) continue;
			for(int j = 1; j <= n; j++) {
				if(j == k || i == j) continue;
				Min[i][j] = max(Min[i][j], Min[i][k] + Min[k][j]); //求最小跑最长
				Max[i][j] = min(Max[i][j], Max[i][k] + Max[k][j]); //求最大跑最短
			}
		}
	}
}
int main() {
	int A, B;
	SF("%d%d%d", &n, &A, &B);
	for(int i = 1; i <= n; i++) {
		for(int j = 1; j <= n; j++) {
			char k;
			cin >> k;
			if(k == '+') Max[i][j] = 2, Min[i][j] = 1;
			else if(k == '=' || i == j) Max[i][j] = Min[i][j] = 0;
			else if(k == '-') Max[i][j] = -1, Min[i][j] = -2;
			else Max[i][j] = 2, Min[i][j] = -2;
		}
	}
	floyd(); //数据很小,保证有解,可以用 floyd
	int c1 = 0, c2 = 0, c3 = 0;
	for(int i = 1; i <= n; i++) {
		if(i == A || i == B) continue;
		for(int j = i + 1; j <= n; j++) {
			if(j == A || j == B) continue;
			if(Min[A][i] > Max[j][B] || Min[A][j] > Max[i][B]) c1++;
			if((Max[A][i] == Min[A][i] && Min[A][i] == Min[j][B] && Min[j][B] == Max[j][B]) || 
			   (Max[A][j] == Min[A][j] && Min[A][j] == Min[i][B] && Min[i][B] == Max[i][B])) c2++;
			if(Max[A][i] < Min[j][B] || Max[A][j] < Min[i][B]) c3++;
		}
	} 
	PF("%d %d %d", c1, c2, c3);
	return 0;
}

3、Intervals

分析:

设前 \(i\) 个数中选了 \(d_i\) 个数。即要使每个 \(d_i\) 尽可能小。

对于第 \(i\) 个条件:\(l_i\)\(r_i\) 中至少选择 \(w_i\) 个数。

即给出了 \(d_{r_i} - d_{l_i - 1} \geq w_i\) 这个限制条件。

转化成 \(d_{l_i - 1} - d_{r_i} \leq -w_i\)。就可以在 \(l_i - 1\)\(r_i\) 之间连接权值为 \(-w_i\) 的有向边。

注意,这个题有隐藏条件!

你一定需要满足的条件是: \(0 \leq d_{i + 1} - d_i \leq 1\)

再根据这个不等式建边就行了。

Code:

#include<bits/stdc++.h>
using namespace std;
#define SF scanf
#define PF printf
struct Edge {
	int to, next, w;
}edge[200005];
int head[50005], cnt, dis[50005], n, s;
bool vis[50005];
void add(int u, int v, int w) {
	edge[++cnt].to = v;
	edge[cnt].next = head[u];
	edge[cnt].w = w;
	head[u] = cnt;
}
void SPFA(int x) {
	queue<int> q;
	q.push(x);
	for(int i = 1; i <= s; i++) dis[i] = 0x3f3f3f3f;
	vis[x] = 1, dis[x] = 0;
	while(!q.empty()) {
		int tmp = q.front();
		q.pop();
		vis[tmp] = 0;
		for(int i = head[tmp]; i; i = edge[i].next) {
			int to = edge[i].to;
			if(dis[to] > dis[tmp] + edge[i].w) {
				dis[to] = dis[tmp] + edge[i].w;
				if(!vis[to]) {
					vis[to] = 1;
					q.push(to); 
				}
			}
		}
	}
}
int main() {
	SF("%d", &n);
	for(int i = 1; i <= n; i++) {
		int u, v, w;
		SF("%d%d%d", &u, &v, &w);
		u++, v++; 
		s = max(s, v);
		add(v, u - 1, -w);
	}
	for(int i = 0; i < s; i++) add(i + 1, i, 0), add(i, i + 1, 1);
	SPFA(s);
	PF("%d", -dis[0]);
	return 0;
}

4、矩阵游戏

分析:

调整法是个不错的方法。

首先我们可以先令第 \(n\) 行和第 \(m\) 列的元素都为 0 ,这样我们就能够得到一组答案 \(a\),虽然此答案可能不合法,但我们可以在其基础上进行调整。

我们设一个变量 \(c\) ,易证在任意一行 \(i\) 中,将第 \(j\) 个元素 \(a_{i,j}\) 变为 \(a_{i,j} + (-1) ^ j * c\),则该答案依旧合法。

再设一个变量 \(g\) ,易证在任意一列 \(r\) 中,将第 \(l\) 个元素 \(a_{l,r}\) 变为 \(a_{l,r} - (-1)^l * g\),则该答案依旧合法。

这样对于任意一个元素 \(a_{i,j}\),就将它变为了 \(a_{i,j} + c - g\)\(a_{i,j} - c + g\)

所以问题就转化成了:求解出各个行的 \(c\) 和各个列的 \(g\) 变量就可以了。

对其进行差分约束即可。

Code:

#include<bits/stdc++.h>
using namespace std;
#define SF scanf
#define PF printf
#define int long long
struct Edge {
	int to, next, w;
}edge[500005];
int a[305][305], b[305][305], head[605], cnt, n, m, dis[605], num[605];
bool vis[100005];
void add(int u, int v, int w) {
	edge[++cnt].to = v;
	edge[cnt].next = head[u];
	edge[cnt].w = w;
	head[u] = cnt;
}
bool SPFA(int x) {
	queue<int> q;
	q.push(x);
	for(int i = 1; i <= n + m; i++) dis[i] = 0x3f3f3f3f;
	dis[x] = 0, vis[x] = 1;
	while(!q.empty()) {
		int tmp = q.front();
		q.pop();
		vis[tmp] = 0;
		for(int i = head[tmp]; i; i = edge[i].next) {
			int to = edge[i].to;
			if(dis[to] > dis[tmp] + edge[i].w) {
				dis[to] = dis[tmp] + edge[i].w;
				if(++num[to] >= n + m) return false;
				if(!vis[to]) {
					vis[to] = 1;
					q.push(to);
				}
			}
		}
	}
	return true;
}
signed main() {
	freopen("matrix.in", "r", stdin);
	freopen("matrix.out", "w", stdout);
	int t;
	SF("%lld", &t);
	while(t--) {
		cnt = 0;
		memset(head, 0, sizeof(head));
		memset(vis, 0, sizeof(vis));
		memset(num, 0, sizeof(num));
		SF("%lld%lld", &n, &m);
		for(int i = 1; i < n; i++) {
			for(int j = 1; j < m; j++) SF("%lld", &b[i][j]);
		}
		for(int i = n - 1; i; i--) {
			for(int j = m - 1; j; j--) a[i][j] = b[i][j] - a[i + 1][j] - a[i][j + 1] - a[i + 1][j + 1];
		}
		for(int i = 1; i <= n; i++) {
			for(int j = 1; j <= m; j++) {
				if((i + j) & 1) {
					// 0 <= aij + ci - gj <= 1e6
					add(i, j + n, a[i][j]);
					add(j + n, i, 1e6 - a[i][j]);
				}
				else {
					// 0 <= aij - ci + gj <= 1e6
					add(j + n, i, a[i][j]);
					add(i, j + n, 1e6 - a[i][j]); 
				}
			}
		}
		bool flag = SPFA(1);
		if(!flag) {
			PF("NO\n");
			continue;
		}
		PF("YES\n");
		for(int i = 1; i <= n; i++) {
			for(int j = 1; j <= m; j++) {
				if((i + j) & 1) PF("%lld ", a[i][j] + dis[i] - dis[j + n]);
				else PF("%lld ", a[i][j] - dis[i] + dis[j + n]);
			}
			PF("\n");
		}
	}
	return 0;
}