差分约束系统

一. 何为差分约束系统?

差分约束系统(system of difference constraints),是求解关于一组变数的特殊不等式组之方法。

  • 如果一个系统由 \(n\) 个变量和 \(m\) 个约束条件组成,其中每个约束条件形如 \(x_i - x_j \le b_k (i, j∈[1, n], k∈[1, m])\) ,则称其为差分约束系统。亦即,差分约束系统是求解关于一组变量的特殊不等式组的方法。

  • 通俗一点地说,差分约束系统就是一些不等式的组,而我们的目标是通过给定的约束不等式组求出最大值或者最小值或者差分约束系统是否有解。


二. 差分约束系统的解

差分约束系统的解只有以下两种情况:

  1. 无解(很显然)

  2. 有无数组解:

先来看一组数据:

\(\begin{cases}x_1 - x_2 \le 1\\x_2 - x_3 \le 2\\x_3 - x_1 \le -1\end{cases}\)

经过暴力枚举依靠我们高超的数感不难得到有其中两组解:

\(\begin{cases}x_1 = 3\\x_2 = 2\\x_3 = 0\end{cases}\)
\(\begin{cases}x_1 = 10\\x_2 = 9\\x_3 = 7\end{cases}\)

这两个解的联系?第二组解中的每个元素比第一组解中的相应元素大 \(7\)

这并不是巧合:若 \(x = ( x1, x2, …, xn )\) 是一个差分约束系统 \(Ax ≤ b\) 的一个解,\(d\) 为任意常数。则 \(x + d = ( x1 + d, x2 + d, …, xn + d )\) 也是该系统 \(Ax ≤ b\) 的解。

对于每个 \(x_i\)\(x_j\) ,有 \((x_j + d) - (x_i + d) = x_j - x_i\) 。因此,若 \(x\) 满足 \(Ax≤b\),则 \(x + d\) 也同样满足,因此,差分约束系统若有解,则必有无数组解。


三. 差分约束系统算法思想

  • 每一个约束条件的不等式 \(x_i - x_j ≤ b_k\)

  • 转换一下:\(x_i ≤ b_k + x_j\)

  • 我们令 \(b_k = w(j, i)\) ,再将不等式中的 \(i\)\(j\) 变量替换掉,\(i = v\)\(j = u\),将 \(x\) 数组的名字改成 \(dis\)

  • 则不等式变为:\(dis[v] ≤ w(u, v) + dis[u]\)

  • 与求单源最短路算法中的松弛操作极为相似:

if (dis[v] > dis[u] + w(u, v))
{
	dis[v] = dis[u] + w(u, v);
}

则松驰完成后的状态必定是 \(dis[v] ≤ w(u, v) + dis[u]\)


四. 三角不等式

1. 求最大值

\(\begin{cases}B - A \le c (1)\\C - B \le a(2)\\C - A \le b(3)\end{cases}\)

  • \(C - A\) 的最大值?

  • 通过 \((1) + (2)\) ,可以得到 \(C - A \le a + c\)

  • 所以就是求 \(min(b, a + c)\),正好对应了 \(A\)\(C\) 的最短路!

  • 对三角不等式加以推广,变量 \(n\) 个,不等式 \(m\) 个,要求 \(X_n - X_1\) 的最大值,便就是求建图后的 \(X_1\)\(X_n\) 最短路。

2. 求最小值

\(\begin{cases}B - A \ge c(1)\\C - B \ge a(2)\\C - A \ge b(3)\end{cases}\)

  • \(C - A\) 的最小值?

  • 等价于求 \(max(b, c + a)\), 即求从 \(A\)\(C\) 的最长路!

  • 同样可以推广到 \(n\) 个变量 \(m\) 个不等式的情况。


四、差分约束系统的难点——建图!!!!!

.

  • 首先根据题目的要求进行不等式组的标准化。

1、要求最小值,即求最长路,将不等式全部化成 \(X_i\) \(–\) \(X_j\) \(\ge\) \(k\) 的形式,这样建立 \(j\) \(\to\) \(i\)的边,权值为 \(k\) 的边。

  1. \(X_i - X_j > k\)
    因为一般题目都是对整形变量的约束, 化为 \(X_i - X_j \ge k + 1\) 即可

  2. \(X_i - X_j = k\)
    可以变为如下两个: \(X_i - X_j \ge k\), \(X_i - X_j \le k\),后者进一步变为\(X_j - X_i \ge -k\),建立两条边即可。

  3. \(a \le X_i - X_j \le b\)
    那么可以转为 \(X_i - X_j \ge a\)\(X_j - X_i \ge -b\)

2、要求最大值,即求最短路,将不等式全部化成 \(X_i - X_j \le k\) 的形式, 这样建立 \(j \to i\) 的边,权值为 \(k\) 的边。

3、判断差分约束系统是否存在解,一般都是是否判断存在负环。

  • 注意:建立的图可能不联通,我们只需要加入一个超级源点。

  • 建好图之后直接跑 \(spfa\) ,因可能存在负边不能用 \(dijkstra\)(危!)。


五、废话不多说,先来看两题:

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

题目描述

给出一组包含 \(m\) 个不等式,有 \(n\) 个未知数的形如:

\(\begin{cases}x_{c_1} - x_{c'_1} \le y_1\\x_{c_2} - x_{c'_2} \le y_2\\\cdots\\x_{c_m} - x_{c'_m} \le y_m\end{cases}\)

的不等式组,求任意一组满足这个不等式组的解。

输入格式

第一行为两个正整数 \(n,m\),代表未知数的数量和不等式的数量。

接下来 \(m\) 行,每行包含三个整数 \(c\)\(c'\)\(y\),代表一个不等式 \(x_c - x_c' \le y\)

输出格式

一行,\(n\) 个数,表示 \(x_1\), \(x_2 \cdots x_n\)的一组可行解,如果有多组解,请输出任意一组,无解请输出 \(NO\)

输入输出样例

输入 #1

3 3

1 2 3

2 3 -2

1 3 1

输出 #1

5 3 5

数据范围

对于 \(100\%\) 的数据,\(1 \le n, m \le 5 \times 10^3, -10^4 \le y \le10^4, 1 \le c, c' \le n, c \ne c'\)


代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;

const int INF = 0x3f;
const int MAX = 1e4 + 5;

int n, m;
int cnt, head[MAX], dis[MAX], vis[MAX], inq[MAX];

struct edge
{
	int to, dis, nxt;
}e[MAX];

void add(int u, int v, int w)
{
	++cnt;
	e[cnt] = {v, w, head[u]};
	head[u] = cnt;
}

bool spfa()
{
	queue <int> q;
	q.push(0);
	memset(dis, INF, sizeof(dis));
	dis[0] = 0;
	++inq[0];
	while (!q.empty())
	{
		int cu = q.front();
		q.pop();
		vis[cu] = 0;
		for (int i = head[cu]; i; i = e[i].nxt)
		{
			int cv = e[i].to, cdis = e[i].dis;
			if (dis[cv] > dis[cu] + cdis)
			{
				dis[cv] = dis[cu] + cdis;
				if (!vis[cv])
				{
					q.push(cv);
					vis[cv] = 1;
					++inq[cv];
					if (inq[cv] > n) //注意:增加超级源点后有(n+1)个点,判断负环的条件变为某个点被松弛超过 n次; 
					{
						return false;
					}
				}
			}
		}
	}
	return true;
}

int main()
{
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; ++i)
	{
		add(0, i, 0); //0号点为超级源点,与每个点相连且距离均为 0,防止了图不连通的情况; 
	}
	for (int i = 1; i <= m; ++i)
	{
		int u, v, w;
		scanf("%d%d%d", &v, &u, &w); //Xi - Xj = y -> Xi = Xj + y, j <=> u, i <=> v, 故先输入v再输入u; 
		add(u, v, w); //建边 
	}
	if (!spfa())
	{
		printf("NO\n");
	}
	else
	{
		for (int i = 1; i <= n; ++i)
		{
			printf("%d ", dis[i]);
		}	
	}
	return 0;
}
/**************************************************************
    Language: C++
    Result: Accepted
    Time:52 ms
    Memory:896 kb
****************************************************************/

2. P1993 小 K 的农场

题目描述

小 K 在 MC 里面建立很多很多的农场,总共 \(n\) 个,以至于他自己都忘记了每个农场中种植作物的具体数量了,他只记得一些含糊的信息(共 \(m\) 个),以下列三种形式描述:

  • 农场 \(a\) 比农场 \(b\) 至少多种植了 \(c\) 个单位的作物;
  • 农场 \(a\) 比农场 \(b\) 至多多种植了 \(c\) 个单位的作物;
  • 农场 \(a\) 与农场 \(b\) 种植的作物数一样多。

但是,由于小 K 的记忆有些偏差,所以他想要知道存不存在一种情况,使得农场的种植作物数量与他记忆中的所有信息吻合。

输入格式

第一行包括两个整数 \(n\)\(m\),分别表示农场数目和小 K 记忆中的信息数目。

接下来 \(m\) 行:

  • 如果每行的第一个数是 \(1\),接下来有三个整数 \(a\),\(b\),\(c\),表示农场 \(a\) 比农场 \(b\) 至少多种植了 \(c\) 个单位的作物;
  • 如果每行的第一个数是 \(2\),接下来有三个整数 \(a\),\(b\),\(c\),表示农场 \(a\) 比农场 \(b\) 至多多种植了 \(c\) 个单位的作物;
  • 如果每行的第一个数是 \(3\),接下来有两个整数 \(a\),\(b\),表示农场 \(a\) 种植的的数量和 \(b\) 一样多。

输出格式

如果存在某种情况与小 K 的记忆吻合,输出 \(Yes\),否则输出 \(No\)

输入输出样例

输入 #1

3 3

3 1 2

1 1 3 1

2 2 3 2

输出 #1

Yes

说明/提示

对于 \(100\%\) 的数据,保证 \(1 \le n,m,a,b,c \le 5 \times 10^3\)

Code

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;

const int MAXN = 1e4 + 5;
const int MAXM = 2e4 + 5;

int n, m;
int cnt, head[MAXN], dis[MAXN], vis[MAXN], k[MAXN];

struct edge
{
	int to, dis, nxt;
}e[MAXM];

void add(int u, int v, int w)
{
	++cnt;
	e[cnt] = {v, w, head[u]};
	head[u] = cnt;
}

bool spfa()
{
	queue <int> q;
	dis[0] = 0;
	vis[0] = 1;
	q.push(0);
	while (!q.empty()) 
	{
		int cu = q.front();
		q.pop();
		vis[cu] = 0;
		for (int i = head[cu]; i; i = e[i].nxt)
		{
			int cv = e[i].to, cdis = e[i].dis;
			if (dis[cv] > dis[cu] + cdis)
			{
				dis[cv] = dis[cu] + cdis;
				k[cv] = k[cu] + 1;
				if (!vis[cv])
				{
					vis[cv] = 1;
					q.push(cv);
					if (k[cv] > n) //这是一种新的判负环的方法,若到达某个点时已走过超过n条边则有负环 
					{
						return false;
					}
				}
			}
		}
	}
	return true;
}

int main()
{
	memset(dis, 0x3f, sizeof(dis));
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= m; ++i)
	{
		int x, u, v, w;
		scanf("%d", &x);
		if (x == 1)
		{
			scanf("%d%d%d", &u, &v, &w);
			add(u, v, -w);
		}
		else if (x == 2)
		{
			scanf("%d%d%d", &u, &v, &w);
			add(v, u, w);
		}
		else if (x == 3)
		{
			scanf("%d%d", &u, &v);
			add(u, v, 0);
			add(v, u, 0);
		}
	}
	for (int i = 1; i <= n; ++i)
	{
		add(0, i, 0);
	}
	if (!spfa())
	{
		puts("No");
	}
	else
	{
		puts("Yes");
	}
	return 0;
}
/**************************************************************
    Language: C++
    Result: Accepted
    Time:138 ms
    Memory:896 kb
****************************************************************/

六. 总结一下

差分约束系统的步骤:

1. 构造不等式

2. 通过不等式连边

3. 建图(难)

4. 跑最短路

posted @ 2021-08-07 17:30  mango09  阅读(364)  评论(0编辑  收藏  举报
-->