差分约束学习笔记

0x00 概念

差分约束系统是一种特殊的 n 元一次不等式组。

差分约束系统 是一种特殊的 n 元一次不等式组,它包含 n 个变量 x1xn 以及 m 个约束条件,每个约束条件是由两个其中的变量做差构成的,形如

xixjck

(1i,jn,ij,1km)

并且 ck 是常数(可以是非负数,也可以是负数)。

我们要解决的问题是:求一组解

x1=a1,x2=a2,,xn=an

使得所有的约束条件得到满足,否则判断出无解。

0x01 求解过程

差分约束系统中的每个约束条件

xixjck

都可以变形成

xixj+ck

这与单源最短路中的三角形不等式

dist[y]dist[x]+z

非常相似。

因此,我们可以把每个变量 xi 看做图中的一个结点,对于每个约束条件 xixjck,从结点 j 向结点 i 连一条长度为 ck 的有向边。

注意到,如果 {a1,a2,,an} 是该差分约束系统的一组解,那么对于任意的常数 d{a1+d,a2+d,,an+d} 显然也是该差分约束系统的一组解,因为这样做差后 d 刚好被消掉。

所以不妨先求一组负数解,即:

假设 i,xi0,这就意味着新建一个 0 号节点,令 x0=0,多了 n 个形如 xix00 的约束条件,应该从 0 号节点向每个节点连一条长度为 0 的有向边。(可以看作一个超级源点)

dist[0]=0,以 0 为起点跑一遍单源最短路,(因为 ck 可能为负数,相当于图中可能会有负权边,所以选用 spfa 算法)。

显然,xi=dist[i] 就是差分约束系统的一组解。

那么无解情况呢?

推论:此差分约束系统无解 图中存在负环。

证明如下:

先证充分性:

若此差分约束系统无解,则一定是出现了 xi<xi 这样的关系,又根据原 m 个不等关系我们可以不断放缩,即

xixj1+ck1

xj1xj2+ck2

xjlenxi+cklen

放缩可得:

xixi+ck1+ck2++cclen

xi<xi

ck1+ck2++cclen<0

对应到图中即:存在一个点数为 len+1 的负环。

充分性得证。

再证必要性:

若图中存在负环,说明这个负环上的变量 xpxqxp<xp+1<<xq<xp,就得到了 xp<xp,很显然矛盾了.

所以如果存在负环,那么给定的差分约束系统无解。

必要性得证。

Q.E.D

0x02 具体例题

P5960 【模板】差分约束

题目大意:

给定一个 n 元不等式,全是 xixjck 的形式,若有解,求出其中一组可行解,否则输出 NO 表示无解。

对于每个关系 xixjck,直接在从 ji 连一条长度为 ck 的有向边。

然后建立一个超级源点,随便取一个基准值 δ 从这个超级源点向每个点连一条长度为 δ 的边,最后在这个超级源点跑一遍 spfa 求最短路就行了。

这里 δ 取的是 0

Code:

#include <queue>
#include <cstring>
#include <iostream>

using namespace std;

const int N = 100010;

int n, m, C;
int h[N], e[N], w[N], ne[N], idx;
int dist[N], cnt[N];
bool vis[N];

void add(int a, int b, int c) {
	e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

bool spfa(int s) {
	queue<int> q;
	q.push(s);
	memset(dist, 0x3f, sizeof dist);
	dist[s] = 0;
	vis[s] = true;
	while(q.size()) {
		int t = q.front();
		q.pop();
		vis[t] = false;
		for(int i = h[t]; ~i; i = ne[i]) {
			int j = e[i];
			if(dist[j] > dist[t] + w[i]) {
				cnt[j] = cnt[t] + 1;
				if(cnt[j] >= n + 1) return false;
				dist[j] = dist[t] + w[i];
				if(!vis[j]) {
					vis[j] = true;
					q.push(j);
				}
			}
		}
	}
	return true;
}

int main() {
	memset(h, -1, sizeof h);
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= n; i++) add(0, i, 0); //建立超级源点并建边
	for(int i = 1, a, b, c; i <= m; i++) {
		scanf("%d%d%d", &a, &b, &c);
		add(b, a, c); //注意a,b的顺序
	}
	if(!spfa(0)) puts("NO");
	else for(int i = 1; i <= n; i++) printf("%d ", dist[i]);
	return 0;
} 

P6145 [USACO20FEB] Timeline G

题目大意:给定一个 n 元不等式,全是 xixjck 的形式,并给定长度为 n 的序列 {s}i[1,n]都有 xiSi 。(满足一定有解)

求出所有 xi 的最小值。

如果采用上一道题的建边方式。

xixjck 变成 xjxick,然后从 ij 连一条长度为 ck 的有向边,这没问题。

但是对于 i[1,n],都有 xiSi 这个条件,建立一个超级源点,令其为 0 号点,且 x0=0,则上式化成:

x0xiSi

那么就应该从 i0 连一条长度为 Si 的有向边,超级源点变成了超级汇点?所以这样做是不行的。

思来想去,考虑到这道题要求每个 xi 的最小值,所以按道理根据这个不等式组,i[1,n],都应该有 xiai,而在求每个 xi 时,可能有多个下界,为使它们全部满足,应该取它们中最大的那个,即:

i[1,n],ximax1pl{ap}

其中 l 是下界个数。

这里就要用到一个结论,即:

求最小值则求最长路;

求最大值则求最短路。

所以这道题应该求最长路。

(所以讲了半天就是为了说明这个结论)

这样建图方式也应相应做出改变。

对于每个关系 xixjck,从 ji 建一条长度为 ck 的有向边。

对于 xiSi,化为 xix0Si,所以从 0 号点向 i 建一条长度为 Si 的有向边。

最后在 0 号点用 spfa 跑一遍最长路即可。

Code:

#include <queue>
#include <cstring>
#include <iostream>

using namespace std;

const int N = 200010;

int n, m, C;
int h[N], e[N], w[N], ne[N], idx;
int dist[N];
bool vis[N];

void add(int a, int b, int c) {
	e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

void spfa(int s) {
	queue<int> q;
	q.push(s);
	memset(dist, -0x3f, sizeof dist);
	dist[s] = 0;
	vis[s] = true;
	while(q.size()) {
		int t = q.front();
		q.pop();
		vis[t] = false;
		for(int i = h[t]; ~i; i = ne[i]) {
			int j = e[i];
			if(dist[j] < dist[t] + w[i]) {
				dist[j] = dist[t] + w[i];
				if(!vis[j]) {
					vis[j] = true;
					q.push(j);
				}
			}
		}
	}
}

int main() {
	memset(h, -1, sizeof h);
	scanf("%d%d%d", &n, &C, &m);
	for(int i = 1, s; i <= n; i++) {
		scanf("%d", &s);
		add(0, i, s);
	}
	for(int i = 1, a, b, c; i <= m; i++) {
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, c);
	}
	spfa(0);
	for(int i = 1; i <= n; i++) printf("%d\n", dist[i]);
	return 0;
} 

从此我们可以总结出一个规律:

两个变量相减那边的建边是反过来的,即 ab 则建边 ba

对于 xixjck 这样的关系,思考跑最长路,执行 add(j, i, ck),或转化成 xjxick,执行add(i, j, -ck)

对于 xixjck 这样的关系,思考跑最短路,执行 add(j, i, ck),或转化成 xjxick,执行add(i, j, -ck)

P1250 种树

这道题非常有意思,因为除了给出的数据需要建边,还有隐藏的建边关系。

注意这句话:

每个部分为一个单位尺寸大小并最多可种一棵树。

这其实隐藏了一个关系:i(1,n],0xixi11

同时,这里 xi 的定义变成了前缀和,所以对于每个关系可以理解为 sum[b]sum[a1]c

综上,再根据刚刚的建图方式,跑最长路。

Code:

#include <queue>
#include <cstring>
#include <iostream>

using namespace std;

const int N = 30010, M = 100010;

int n, m, C;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
bool vis[N];

void add(int a, int b, int c) {
	e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

int ans;
void spfa(int s) {
	queue<int> q;
	q.push(s);
	memset(dist, -0x3f, sizeof dist);
	dist[s] = 0;
	vis[s] = true;
	while(q.size()) {
		int t = q.front();
		q.pop();
		vis[t] = false;
		for(int i = h[t]; ~i; i = ne[i]) {
			int j = e[i];
			if(dist[j] < dist[t] + w[i]) {
				dist[j] = dist[t] + w[i];
				if(!vis[j]) {
					vis[j] = true;
					q.push(j);
				}
			}
		}
	}
}

int main() {
	memset(h, -1, sizeof h);
	scanf("%d%d", &n, &m);
	for(int i = 0; i <= n; i++) {
		if(i) add(i - 1, i, 0);
		if(i < n) add(i, i - 1, -1);
	}
	for(int i = 1, a, b, c; i <= m; i++) {
		scanf("%d%d%d", &a, &b, &c);
		add(a - 1, b, c);
	}
	spfa(0);
	printf("%d", dist[n]);
	return 0;
} 

P3275 [SCOI2011] 糖果

这道题就更有意思了,(看上去像数据结构似的)。

首先发现要求最小值,所以跑最长路,并将所有关系都转化成大于等于。

一共有五种关系,分类讨论:

第一种操作:xa=xb根据 whk 上经常使用的方法可以转化为 xaxbxaxb,所以在 a,b 间连一条长度为 0 的无向边。

第二种操作:xa<xb,由于糖果数一定是整数,所以转化为 xbxa1,所以从 ab 连一条长度为 1 的有向边。

第三种操作:xaxb,转化为 xaxb0,所以从 ab 连一条长度为 0 的有向边。

第四种操作:xa>xb,由于糖果数一定是整数,所以转化为 xaxb1,所以从 ba 连一条长度为 1 的有向边。

第五种操作:xaxb,转化为 xaxb0,所以从 ab 连一条长度为 0 的有向边。

考虑到每个小朋友都要拿到糖,所以建立一个超级源点 0,向每个点连一条长度为 1 的边。

最后在 0 号点跑 spfa 求最长路,累加答案。

Code:

#include <queue>
#include <cstring>
#include <iostream>

using namespace std;

const int N = 100010, M = 200010;

int n, m;
int h[N], e[M << 1], w[M << 1], ne[M << 1], idx;
int dist[N], cnt[N];
bool vis[N];

void add(int a, int b, int c) {
	e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

bool spfa(int s) {
	memset(dist, -0x3f, sizeof dist);
	dist[s] = 0;
	queue<int> q;
	q.push(s);
	vis[s] = true;
	while(q.size()) {
		int t = q.front();
		q.pop();
		vis[t] = false;
		for(int i = h[t]; ~i; i = ne[i]) {
			int j = e[i];
			if(dist[j] < dist[t] + w[i]) {
				cnt[j] = cnt[t] + 1;
				if(cnt[j] >= n + 1) return false;
				dist[j] = dist[t] + w[i];
				if(!vis[j]) {
					vis[j] = true;
					q.push(j);
				}
			}
		}
	}
	return true;
}

int main() {
	memset(h, -1, sizeof h);
	scanf("%d%d", &n, &m);
	int op, a, b;
	for(int i = 1; i <= n; i++) add(0, i, 1);
	for(int i = 1; i <= m; i++) {
		scanf("%d%d%d", &op, &a, &b);
		if(op == 1) add(a, b, 0), add(b, a, 0);
		else if(op == 2) add(a, b, 1);
		else if(op == 3) add(b, a, 0);
		else if(op == 4) add(b, a, 1);
		else add(a, b, 0);
	}
	if(!spfa(0)) puts("-1");
	else {
		int ans = 0;
		for(int i = 0; i <= n; i++) ans += dist[i];
		printf("%d\n", ans);
	}
	return 0;
}
posted @   Brilliant11001  阅读(10)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示