其他分享
首页 > 其他分享> > 差分约束系统 学习笔记

差分约束系统 学习笔记

作者:互联网

第4章 差分约束系统

算法简述

核心思想:将题目给定条件转化为形如\(s[i] - s[j] <= k\)的三角形不等式,然后用最短路求解

解题要点:感觉还是<=跑最短路好写点,据此,
在取\(a[i] - a[j] <= k\)的形式时,规则如下:

  1. 差分约束跑最短路,跑出的结果是所有解中的最大解
  2. 差分约束跑最长路,跑出的结果是所有解中的最小解

1509:【例 1】Intervals

link

Sol

记s[i]为0-i区间选取整数数量,根据题意,\(s[b[i]] - s[a[i] - 1] >= c[i]\)

只有这一个条件不足以构成差分约束,发现不能有重复数字,即\(s[i + 1] - s[i] <= 1\),转换就是\(s[i] - s[i + 1] >= -1\)

显然还有\(s[i + 1] >= s[i]\),即\(s[i + 1] - s[i] >= 0\)

条件足够,注意到\(a[i] - b[i] + 1 >= c[i]\),所以不会出现正环,定有解,以此为三角形不等式构造单源最长路即可。

如果还要输出路径序列,只要比较最终得到的s[i]和s[i - 1]是否相同,如果不同,说明选了i,输出i即可。

Code

#include <bits/stdc++.h>

using namespace std;

const int MN = 5e4 + 5;
const int INF = 0x3f3f3f3f;

int n, cnt, minn, maxx;
int h[MN], nxt[MN * 3], ver[MN * 3], dist[MN * 3];
int dis[MN];
bool inq[MN];

void add_edge(int x, int y, int z) {
	cnt++;
	ver[cnt] = y, dist[cnt] = z, nxt[cnt] = h[x], h[x] = cnt;
}

int spfa(int st) {
	for (int i = minn; i <= maxx; i++) {
		dis[i] = -INF;
	}
	dis[st] = 0;
	queue<int> q;
	q.push(st);
	inq[st] = true;
	while (!q.empty()) {
		int x = q.front();
		q.pop();
		inq[x] = false;
		for (int i = h[x], y, z; i; i = nxt[i]) {
			y = ver[i], z = dist[i];
			if (dis[x] + z > dis[y]) {
				dis[y] = dis[x] + z;
				if (!inq[y]) {
					q.push(y);
					inq[y] = true;
				}
			}
		}
	}
	return dis[maxx];
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin >> n;
	minn = INF;
	maxx = -1;
	for (int i = 1, x, y, z; i <= n; i++) {
		cin >> x >> y >> z;
		add_edge(x - 1, y, z);
		minn = min(minn, x - 1);
		maxx = max(maxx, y);
	}
	for (int i = minn; i <= maxx; i++) {
		add_edge(i, i + 1, 0);
		add_edge(i + 1, i, -1);
	}
	cout << spfa(minn) << '\n';
/*
	for (int i = minn; i <= maxx; i++) {
		if (dis[i] != dis[i - 1]) {
			cout << i << ' ';
		}
	}
*/
	return 0;
}

1510:【例 2】出纳员问题

link

Sol

设x[i]为第i刻最终雇佣人数,num[i]为第i刻能够开始人数,r[i]为第i刻需要人数

\(s[i] = sum(x[j]) (j = 1\) ~ \(i)\)
\(s[i] - s[i - 1] <= num[i]\)
\(s[i - 1] - s[i] >= -num[i] (1 <= i <= 24)\)
当日的最终使用人选一定小于等于(不大于)当日需要人数(可以继续沿用前七天内的)

\(s[i] - s[i - 8] >= r[i] (9 <= i <= 24)\)
总共的人选一定大于等于(不小于)需要人数

\(s[i] - s[i - 1] >= 0 (1 <= i <= 24)\)
第i时的使用人数一定大于等于(不小于)前一时的人数

\(s[24] + s[i] - s[24 - 8 + i] = s[24] - s[16 + i] + s[i] >= r[i] (1 <= i <= 8)\)
\(s[i] - s[16 + i] >= r[i] - s[24]\)
跨半夜的情况,加上第二天早晨的s[i],减掉第一天上午和下午的s[16 + i],如此计算得出可用总共人选一定大于等于(不小于)需要人数

需要判断出现正环的情况:一共只有24个点,所以路径最多只能经过24个点,如果一个点被入队了24次就判断出现正环

注意多组数据!!!

Code

#include <bits/stdc++.h>

using namespace std;

const int INF = 0x3f3f3f3f;

int n, cnt;
int r[30], t[1005], num[30], dis[30], tag[30];
int h[30], ver[30 * 4], nxt[30 * 4], dist[30 * 4];
bool inq[30];

void add_edge(int x, int y, int z) {
	cnt++;
	ver[cnt] = y, dist[cnt] = z, nxt[cnt] = h[x], h[x] = cnt;
}

bool spfa(int ans) {
	for (int i = 0; i <= 25; i++) {
		dis[i] = -INF;
		inq[i] = false;
		tag[i] = 0;
	}
	queue<int> q;
	q.push(0);
	dis[0] = 0;
	inq[0] = true;
	tag[0] = 1;
	while (!q.empty()) {
		int x = q.front();
		q.pop();
		inq[x] = false;
		for (int i = h[x], y, z; i; i = nxt[i]) {
			y = ver[i], z = dist[i];
			if (dis[x] + z > dis[y]) {
				dis[y] = dis[x] + z;
				if (!inq[y]) {
					q.push(y);
					inq[y] = true;
					tag[y]++;
					if (tag[y] > 24) {
						return false;
					}
				}
			}
		}
	}
	if (dis[24] == ans) {
		return true;
	} else {
		return false;
	}
}

bool solve(int x) {
	memset(h, 0, sizeof h);
	cnt = 0;
	add_edge(0, 24, x);
	for (int i = 1; i <= 24; i++) {
		add_edge(i - 1, i, 0);
		add_edge(i, i - 1, -num[i]);
	}
	for (int i = 9; i <= 24; i++) {
		add_edge(i - 8, i, r[i]);
	}
	for (int i = 1; i <= 8; i++) {
		add_edge(i + 16, i, r[i] - x);
	}
	if (spfa(x)) {
		cout << x << '\n';
		return true;
	} else {
		return false;
	}
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	int tt;
	cin >> tt;
	while (tt--) {
		for (int i = 1; i <= 24; i++) {
			cin >> r[i];
		}
		memset(num, 0, sizeof num);
		cin >> n;
		for (int i = 1; i <= n; i++) {
			cin >> t[i];
			num[t[i] + 1]++;
		}
		bool flag = false;
		for (int i = 0; i <= n; i++) {
			if (solve(i)) {
				flag = true;
				break;
			}
		}
		if (!flag) {
			cout << "No Solution" << '\n';
		}
	}
	return 0;
}

1511:【SCOI2011】糖果

link

Sol

按照小盆友的关系正常建图,由于是点对点的关系,可能没联通,所以我们建一个超级汇点0,把所有点都串在一块就行。

注意建超级汇点的时候需要从n到1建,不然会T一个点

Code

#include <bits/stdc++.h>

using namespace std;

int read() {
	int x = 0; char ch; bool f = true;
	while (!isdigit(ch = getchar())) {
		f = ch != '-';
	}
	do x = x * 10 + (ch & 15); while (isdigit(ch = getchar()));
	return f ? x : -x;
}

const int MN = 1e5 + 5;
const int INF = 0x3f3f3f3f;

int n, k, cnt, qwq;
int h[MN], nxt[MN * 4], ver[MN * 4], dist[MN * 4];
int dis[MN], tag[MN];
bool inq[MN];
vector<int> date;

void add_edge(int x, int y, int z) {
	cnt++;
	ver[cnt] = y, dist[cnt] = z, nxt[cnt] = h[x], h[x] = cnt;
}

void spfa(int st) {
	queue<int> q;
	q.push(st);
	inq[st] = true;
	tag[st] = 1;
	while (!q.empty()) {
		int x= q.front();
		q.pop();
		inq[x] = false;
		for (int i = h[x], y, z; i; i = nxt[i]) {
			y = ver[i], z = dist[i];
			if (dis[x] + z > dis[y]) {
				dis[y] = dis[x] + z;
				if (!inq[y]) {
					q.push(y);
					inq[y] = true;
					tag[y]++;
					if (tag[y] > n) {
						cout << -1 << '\n';
						exit(0);
					}
				}
			}
		}
	}
}

int main() {
	n = read(), k = read();  
	for (int i = 1, x, a, b; i <= k; i++) {
		x = read(), a = read(), b = read();
		if (x == 1) {
			add_edge(a, b, 0);
			add_edge(b, a, 0);
		} else if (x == 2) {
			if (a == b) {
				cout << -1 << '\n';
				return 0;
			}
			add_edge(a, b, 1);
		} else if (x == 3) {
			add_edge(b, a, 0);
		} else if (x == 4) {
			if (a == b) {
				cout << -1 << '\n';
				return 0;
			}
			add_edge(b, a, 1);
		} else if (x == 5) {
			add_edge(a, b, 0);
		}
	}
	for (int i = n; i >= 1; i--) {
		add_edge(0, i, 1);
	}
	spfa(0);
	long long ans = 0;
	for (int i = 1; i <= n; i++) {
		ans += dis[i];
	}
	cout << ans << '\n';
	return 0;
}

1512:排队布局

link
double link

杂话

一本通上的取\(>=\)跑最长路写法在写这道题时一堆bug,还是\(<=\)跑最短路好。。。
在取\(s[i] - s[j] <= k\)的形式时,规则如下:

  1. 差分约束跑最短路,跑出的结果是所有解中的最大解
  2. 差分约束跑最长路,跑出的结果是所有解中的最小解
    (摘自This Blog

Sol

考虑还是建一个超级汇点,但与之前那题不同之处在于这题每个点除了输入点对给定的限制关系之外,还存在包含与被包含的判断关系,所以每个点必须和其余n - 1个点都联通,而我们如果只建立超级汇点,是不能保证所有点都联通的,所以我们需要给第i + 1点和第i点连边。注意是从i + 1到i连边,不是从i到i + 1,因为输入时连的边都是从大往小连,此处要考虑负权回路的情况

Code

#include <bits/stdc++.h>

using namespace std;

const int MN = 1e4 + 5;
const int INF = 0x3f3f3f3f;

int n, m1, m2, cnt;
int h[MN], nxt[MN * 4], ver[MN * 4], dist[MN * 4];
int dis[MN], tag[MN];
bool inq[MN];

void add_edge(int x, int y, int z) {
	cnt++;
	ver[cnt] = y, dist[cnt] = z, nxt[cnt] = h[x], h[x] = cnt;
}

int spfa(int st) {
	memset(dis, 0x7f / 3, sizeof dis);
	memset(inq, false, sizeof inq);
	memset(tag, 0, sizeof tag);
	dis[st] = 0;
	queue<int> q;
	q.push(st);
	inq[st] = true;
	tag[st] = 1;
	while (!q.empty()) {
		int x = q.front();
		q.pop();
		inq[x] = false;
		for (int i = h[x], y, z; i; i = nxt[i]) {
			y = ver[i], z = dist[i];
			if (dis[x] + z < dis[y]) {
				dis[y] = dis[x] + z;
				if (!inq[y]) {
					q.push(y);
					inq[y] = true;
					tag[y]++;
					if (tag[y] > n) {
						return -1;
					}
				}
			}
		}
	}
	if (dis[n] > 1e8) {
		return -2;
	} else {
		return dis[n];
	}
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin >> n >> m1 >> m2;
	for (int i = 1, x, y, z; i <= m1; i++) {
		cin >> x >> y >> z;
		add_edge(x, y, z);
	}
	for (int i = 1, x, y, z; i <= m2; i++) {
		cin >> x >> y >> z;
		add_edge(y, x, -z);
	}
	for (int i = n; i >= 1; i--) {
		add_edge(0, i, 0);
	}
	for (int i = n - 1; i >= 1; i--) {
		add_edge(i + 1, i, 0);
	}
	int p = spfa(0);
	if (p == -1 || p == -2) {
		cout << p << '\n';
	} else {
		cout << spfa(1) << '\n';
	}
	return 0;
}

好文章

https://www.cnblogs.com/Eleven-Qian-Shan/p/13226903.html

标签:cnt,int,MN,inq,笔记,约束,tag,差分,dis
来源: https://www.cnblogs.com/zjsqwq/p/16513167.html