差分约束系统小结

一、什么是差分约束系统?

1. 引例

\[x_1-x_0\le 2 ......(1) \]

\[x_2-x_0\le 7 ......(2) \]

\[x_3-x_0\le 8 ......(3) \]

\[x_2-x_0\le 3 ......(4) \]

\[x_3-x_2\le 2 ......(5) \]

给定\(n\)个变量和\(m\)个不等式,每个不等式形如$x_i - x_j <= a_k $$(0 \le i, j < n, 0 \le k < m, a_k\(已知\))\(,求\)x_{n-1} - x_0\(的最大值。例如当\)n = 4\(,\)m = 5\(,不等式组如图所示的情况,求\)x_3-x_0$的最大值。

我们考虑暴力。

  1. \((3)\)\(x_3 - x_0 \le 8\)
  2. \((2)+(5)\)\(x_3-x_0\le 9\)
  3. \((1)+(4)+(5)\)\(x_3-x_0\le 7\)

所以有\(x_3-x_0\le min\{7,8,9\}=7\),即\(x_3-x_0\)的最小值为\(7\)

当然,对于更大的数据,我们希望能用算法系统地处理,想一想,我们暴力计算的过程跟什么有点像?

看图,如果让我们求这张图的最短路,我们也许很快就能敲完一份完美的代码。

再仔细看看这张图,你会发现,这张图似乎与前面的不等式限制条件一一对应。

没错,我们需要用图论的知识把不等式关系转换成图中点与边之间的关系。

如若一个系统由\(n\)个变量和\(m\)个不等式组成,并且这\(m\)个不等式对应的系数矩阵中每一行有且仅有一个\(1\)\(-1\),其它的都为\(0\),这样的系统称为差分约束( difference constraints )系统。前面的不等式组同样可以表示成如图的系数矩阵。(摘自夜深人静写算法系列

\[\left[ \begin{matrix} -1 & 1 & 0 & 0 \\ -1 & 0 & 1 & 0 \\ -1 & 0 & 0 & 1 \\ 0 & -1 & 1 & 0 \\ 0 & 0 & -1 & 1 \end{matrix} \right] \left( \begin{matrix} x_0 \\ x_1 \\ x_2 \\ x_3 \\ \end{matrix} \right) \le \left( \begin{matrix} 2 \\ 7 \\ 8 \\ 3 \\ 2 \end{matrix} \right) \]

现在,我们思考如何转换。

二、代数与图形的关系及原理

1.原理的感性理解

对于一个单独的不等式\(x_i-x_j\le a_k\)来说,将这个等式移项,可得\(x_i\le x_j+a_k\),这与单源最短路问题中的\(dis[v]\le dis[u]+w(u,v)\)很像啊。

也就是说,在求最短路时,如果出现\(dis[v]>dis[u]+w(u,v)\),我们就会更新\(dis[v]\),力求\(dis[v]\le dis[u]+w(u,v)\),这不与我们求解不等式的解如出同辙吗?

也就是说,形如\(x_i-x_j\le a_k\)的式子,我们连一条由\(j\)\(i\),权值为\(a_k\)的有向边,然后在图上跑一遍最短路,就能得到我们想要的答案。

2. 三角不等式

我们来看一个简化的情况。

\[x_B-x_A\le d_1......(1) \]

\[x_C-x_B\le d_2......(2) \]

\[x_C-x_A\le d_3......(3) \]

我们同样想知道\(x_C-x_A\)的最大值,\((1)+(2)\),可得\(x_C-x_A\le d_1+d_2\),再有个\((3)\),所以实质上我们在求\(min\{(d_1+d_2),d_3\}\),把这个建个图你就会发现,\(min\{(d_1+d_2),d_3\}\)正好对应了\(A\)\(C\)的最短路。

3. 解的有无

因为我们已成功地将不等式关系转化成了图中的关系,所以当图异常结束的时候,就说明原不等式组无解。这里的异常结束包括最短路中的负权环,最长路中的正权环。

当然,若未更新我们想知道的点的值,就说明它未在差分约束系统里,所有取值都是可行的

只要最短(长)路算法正常结束并且所有变量都没有确定的值,那么该不等式必然有无限多组解,并且当前所求出的一组解必然是满足题目条件的边界解;一般情况下,我们会新建一个节点值为\(0\),与其他点相连,有时题目会给出某些初始点的值。

4. 最大值与最小值的转化

前面我们有意无意提到了最长路,也就是说,我把前面不等式反号,把\(\le\)变成\(\ge\),照样可以建图,然后跑最长路,思想与前面一样。

当然,也可以通过数学方法,两边乘个\(-1\),变成前面的最短路处理。

三、差分约束的应用&题目

1. 线性约束:布局Layout

大意:

一维线上有\(n(2\le n\le 1000)\)个点,有些点之间距离不能大于某个数,也有些点之间距离不能小于某个数,求第\(n\)个点到第\(1\)个点的距离最小是多少,若没有合法情况,输出\(-1\),若可以取无限大,输出\(-2\)

分析:

\(P_i\)表示\(i\)号位置到\(1\)的距离,那么我们可以知道,\(P_i\)单调不下降。

所以有第一类限制条件:\(P_1\le P_2\le ...\le P_{n-1}\le P_n\)

同时,它给出了\(M_L\)\(P_i-P_j\le D\)\(M_D\)\(P_i-P_j\ge D\),直接连边。这里的\(P_1\)为0,求\(P_n\)即为最终答案。

有个坑点,首先应先判断整张图有没有环,即新建节点\(0\)连向所有节点,这样也不会对后面计算答案产生影响并且可以遍历完整张图。

然后再从\(1\)节点跑,若\(P_n\)未被更新,就说明最短路无限长,否则输出最短路的长度。

代码:

#include <queue>
#include <vector>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define Re register
using namespace std;
const int MAX = 1e4 + 5;
const int INF = 0x7f7f7f7f;
const int RS = -2139062144;
inline int read(){
    int f = 1, x = 0; char ch;
    do { ch = getchar(); if (ch == '-') f = -1; } while (ch < '0' || ch > '9');
    do {x = (x << 3) + (x << 1) + ch - '0'; ch = getchar(); } while (ch >= '0' && ch <= '9'); 
    return f * x;
}
struct Sakura { int to, nxt, w; }sak[MAX << 1]; int head[MAX], cnt;
inline void add(int x, int y, int w) {
    ++cnt;
    sak[cnt].to = y, sak[cnt].w = w, sak[cnt].nxt = head[x], head[x] = cnt;
}

int n, ml, md;

struct SPFA {
	int dis[MAX], cnt[MAX], vis[MAX];
    inline bool Run(int st) {
    	for (int i = 0;i <= n; ++i) dis[i] = 1e9, cnt[i] = 0, vis[i] = 0;
    	queue <int> Q;
        dis[st] = 0;
        vis[st] = 1;
        Q.push(st);
        while (!Q.empty()) {
            int u = Q.front();
            Q.pop();
            vis[u] = 0;
            ++cnt[u];
            if (cnt[u] >= n) {
            	puts("-1");
            	exit(0);
            }
            for (int i = head[u];i;i = sak[i].nxt) {
                int v = sak[i].to, w = sak[i].w;
                if (dis[v] > dis[u] + w) {
                    dis[v] = dis[u] + w;
                    if (!vis[v]) {
                        Q.push(v);
                        vis[v] = 1;
                    }
                }
            }
        }
        return 1;
    }
}Spfa;

int main(){
	n = read(), ml = read(), md = read();
	for (int i = 1;i <= ml; ++i) {
		int a = read(), b = read(), d = read();
		add(a, b, d);
	}
	for (int i = 1;i <= md; ++i) {
		int a = read(), b = read(), d = read();
		add(b, a, -d);
	}
	for (int i = 2;i <= n; ++i) {
		add(i, i - 1, 0);
	}
	for (int i = 1;i <= n; ++i) {
		add(0, i, 0);
	}
	Spfa.Run(0);
	Spfa.Run(1);
	if (Spfa.dis[n] == 1e9) {
		printf("-2");
		return 0;
	}
	else printf("%d", Spfa.dis[n]);
    return 0;
}

2. 区间约束:Intervals

大意:

给定\(n\)个区间约束条件,要求\([a_i,b_i]\)中至少有\(c_i\)个数,求构造出的总个数最少。

分析:

区间上,设\(m_j\)表示区间为\([j,j]\)上的个数为多少,即有:\(0\le m_j\le 1\),且\(\sum_{j=a_i}^{b_i}m_j\le c_i\)

考虑前缀和\(S_j\),所以就有:\(0\le S_j-S_{j-1}\le 1\)\(S_{b_i}-S_{a_i-1}\le c_i\)

然后就可以做了。

#include <queue>
#include <vector>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define Re register
using namespace std;
const int MAX = 200000 + 5;
const int INF = 0x7f7f7f7f;
inline int read(){
    int f = 1, x = 0; char ch;
    do { ch = getchar(); if (ch == '-') f = -1; } while (ch < '0' || ch > '9');
    do {x = (x << 3) + (x << 1) + ch - '0'; ch = getchar(); } while (ch >= '0' && ch <= '9'); 
    return f * x;
}
struct Sakura { int to, nxt, w; }sak[MAX << 1]; int head[MAX], cnt;
inline void add(int x, int y, int w) {
    ++cnt;
    sak[cnt].to = y, sak[cnt].w = w, sak[cnt].nxt = head[x], head[x] = cnt;
}

int t, n, maxx, minn = INF;

struct SPFA {
	int dis[MAX];
    int vis[MAX]; queue <int> Q;
    inline void Run(int st) {
        for (int i = 0;i <= maxx; ++i) dis[i] = -INF;
        memset(vis, 0, sizeof vis);
        dis[st] = 0;
        vis[st] = 1;
        Q.push(st);
        while (!Q.empty()) {
            int u = Q.front();
            Q.pop();
            vis[u] = 0;
            for (int i = head[u];i;i = sak[i].nxt) {
                int v = sak[i].to, w = sak[i].w;
                if (dis[v] < dis[u] + w) {
                    dis[v] = dis[u] + w;
                    if (!vis[v]) {
                        Q.push(v);
                        vis[v] = 1;
                    }
                }
            }
        }
    }
}Spfa;

int main(){
	t = read();
	while (t --) {
		n = read();
		memset(head, 0, sizeof head);
		cnt = 0;
		for (int i = 1;i <= n; ++i) {
			int a = read() + 1, b = read() + 1, w = read();
			minn = min(minn, a - 1);
			maxx = max(maxx, b);
			add(a - 1, b, w);
		}
		for (int i = 1;i <= maxx; ++i) add(i - 1, i, 0), add(i, i - 1, -1);
		Spfa.Run(0);
		printf("%d\n", Spfa.dis[maxx]);		
		if (t != 0) puts("");
	}
    return 0;
}

3. 未知约束:出纳员问题

大意:略

分析:

限制条件有点多,慢慢来整理一下。

\(num_i\)表示在\(i\)时刻最多能招到多少人,\(R_i\)表示在\(i\)时刻至少需要多少人。

首先,设\(a_i\)表示在\(i\)时刻雇佣的人数,则有\(0\le a_i\le num_i\)

其次,有\(a_{i-7}+a_{i-6}+...+a_i\ge R_i(7\le i\le 23)\),还有\(a_0+a_1+...+a_i+a_{i+17}+...+a_{23}\ge R_i\)\((0\le i< 7)\)

\(S_i\)\(a_i\)的前缀和,所以就有:

  1. \(0\le S_i-S_{i-1}\le num[i](0\le i\le 23)\)
  2. \(S_i-S_{i-8}\ge R_i(8\le i\le 23)\)
  3. \(S_i-S_{i+16}\ge R_i-S_{23}(0\le i< 8)\)

前两个好办,关键是第三个。

看到我把\(S_{23}\)移到了右边,也许你会猜到什么了吧?

没错,枚举\(S_{23}\),从\(0\)\(n\)枚举,看是否出现可行解,当然记得要确保图中的节点\(23\)的值就是你枚举的值,我们可以新建一个值为\(0\)的节点\(h\),就有\(S_{23}-S_h=\)你枚举的值,把它拆成两个不等式,搞定。

然后,思考,当招\(i\)个人能满足要求时,\(i+1\)个人也能满足要求,直接二分求解。

#include <queue>
#include <vector>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define Re register
using namespace std;
const int MAX = 200000 + 5;
const int INF = 0x7f7f7f7f;
inline int read(){
    int f = 1, x = 0; char ch;
    do { ch = getchar(); if (ch == '-') f = -1; } while (ch < '0' || ch > '9');
    do {x = (x << 3) + (x << 1) + ch - '0'; ch = getchar(); } while (ch >= '0' && ch <= '9'); 
    return f * x;
}
struct Sakura { int to, nxt, w; }sak[MAX << 1]; int head[MAX], cnt;
inline void add(int x, int y, int w) {
    ++cnt;
    sak[cnt].to = y, sak[cnt].w = w, sak[cnt].nxt = head[x], head[x] = cnt;
}

int T, R[MAX], num[MAX],  n;

struct SPFA {
	int dis[MAX], cnt[MAX], vis[MAX];
    inline bool Run(int st) {
    	memset(dis, 128, sizeof dis);
    	memset(cnt, 0, sizeof cnt);
    	memset(vis, 0, sizeof vis);
    	queue <int> Q;
        dis[st] = 0;
        vis[st] = 1;
        Q.push(st);
        while (!Q.empty()) {
            int u = Q.front();
            Q.pop();
            vis[u] = 0;
            for (int i = head[u];i;i = sak[i].nxt) {
                int v = sak[i].to, w = sak[i].w;
                if (dis[v] < dis[u] + w) {
                    dis[v] = dis[u] + w;
                    if (!vis[v]) {
                        Q.push(v);
                        vis[v] = 1;
                        if (++cnt[v] > n) return 0;
                    }
                }
            }
        }
        return 1;
    }
}Spfa;

int main(){
    T = read();
    while (T --) {
    	bool can = 0;
    	for (int i = 1;i <= 24; ++i) {
    		R[i] = read();
			if (R[i]) can = 1;
    	}
		for (int i = 0;i <= 24; ++i) num[i] = 0;
    	n = read();
    	for (int i = 1;i <= n; ++i) {
    		int x = read() + 1;
    		num[x] ++;
    	}
    	if (!can) {
    		printf("0\n");
    		continue;
    	}
    	bool flag = 0;
    	int l = 0, r = n;
    	while (l < r) {
    		int mid = (l + r) >> 1;
    		memset(head, 0, sizeof head);
    		cnt = 0;
    		for (int i = 9;i <= 24; ++i) add(i - 8, i, R[i]);
    		for (int i = 1;i <= 8; ++i) add(i + 16, i, R[i] - mid);
    		for (int i = 1;i <= 24; ++i) add(i, i - 1, -num[i]), add(i - 1, i, 0);
			add(0, 24, mid);
			add(24, 0, -mid);
			if (Spfa.Run(0)) r = mid, flag = 1;
    		else l = mid + 1;
    	}
    	if (flag) printf("%d\n", r);
    	else {
    		memset(head, 0, sizeof head);
    		cnt = 0;
     		for (int i = 9;i <= 24; ++i) add(i - 8, i, R[i]);
    		for (int i = 1;i <= 8; ++i) add(i + 16, i, R[i] - r);
    		for (int i = 1;i <= 24; ++i) add(i, i - 1, -num[i]), add(i - 1, i, 0);
			add(0, 24, r);
			add(24, 0, -r);			
			if (Spfa.Run(0)) printf("%d\n", r);
			else printf("No Solution\n");
    	}
	}
    return 0;
}

4. 合理转化:糖果

大意:

略略略~

分析:

把前面弄懂就会做了。

代码:

#include<cstdio>
#include<cstdlib>
#include<cstring>
#define ll long long
#define Re register
#define Min(a, b) ((a) < (b) ? (a) : (b)) 
const int MAX = 500000 + 5;
inline int read(){
    int f = 1, x = 0;char ch;
    do { ch = getchar(); if (ch == '-') f = -1; } while (ch < '0'||ch>'9');
    do {x = x*10+ch-'0'; ch = getchar(); } while (ch >= '0' && ch <= '9'); 
    return f*x;
}
 
struct sakura {
    int to, nxt, w;
}sak[MAX]; int head[MAX], cnt;
inline void add(int x, int y, int w) {
    ++cnt;
    sak[cnt].to = y, sak[cnt].nxt = head[x], sak[cnt].w = w, head[x] = cnt;
}
ll ans;
int n, k, q[MAX], h, t = 1, dis[MAX], vis[MAX], cnts[MAX];
inline bool SPFA(int st) {
    q[1] = st;
    dis[st] = 0;
    vis[st] = 1;
     
    while (h < t) {
        int u = q[++h];
        cnts[u]++;
        if (cnts[u] > n - 1) return 0;
        vis[u] = 0;
        for (int i = head[u];i;i = sak[i].nxt) {
            int v = sak[i].to, w = sak[i].w;
            if (dis[v] < dis[u] + w) {
                dis[v] = dis[u] + w;
                if (!vis[v]) q[++t] = v, vis[v] = 1;
            }
        }
    }
    return 1;
}
 
int main(){
    n = read(), k = read();
    for (int i = 1;i <= k; ++i) {
        int x = read(), a = read(), b = read();
        switch(x) {
            case 1:{
                add(a, b, 0);
                add(b, a, 0);
                break;
            } 
            case 2:{
                if (a == b) {
                    puts("-1");
                    return 0;
                }
                add(a, b, 1);
                break;
            }
            case 3:{
                add(b, a, 0);
                break;
            }
            case 4:{
                if (a == b) {
                    puts("-1");
                    return 0;
                }
                add(b, a, 1);
                break;
            }
            case 5:{
                add(a, b, 0);
                break;
            }
        }
    }
    for (int i = n;i >= 1; --i) {
        add(n + 1, i, 1);
    }
    if (SPFA(n + 1)) {
        for (int i = 1;i <= n; ++i) {
            ans += dis[i];
        }
        printf("%lld", ans);
    }
    else printf("-1");
    return 0;
}
posted @ 2019-08-29 20:41  SilentEAG  阅读(131)  评论(0编辑  收藏  举报