赞
踩
差分约束是一种特殊的N元一次不等式,它包含N个变量X1~Xn以及M个约束条件,每个约束条件都是由两个变量作差构成的,形如Xi-Xj≤Ck,其中Ck是常数(正负均可),1≤i,j≤N,1≤k≤M。我们要解决的问题就是,求一组解X1=a1,X2=a2······Xn=an,使所有的约束条件得到满足。
1、一类不等式组的解
给定n个变量和m个不等式,每个不等式形如 x[i] - x[j] <= a[k] (0 <= i, j < n, 0 <= k < m, a[k]已知),求 x[n-1] - x[0] 的最大值。例如当n = 4,m = 5,不等式组如图一-1-1所示的情况,求x3 - x0的最大值。
观察x3 - x0的性质,我们如果可以通过不等式的两两加和得到c个形如 x3 - x0 <= Ti 的不等式,那么 min{ Ti | 0 <= i < c } 就是我们要求的x3 - x0的最大值。于是开始人肉,费尽千辛万苦,终于整理出以下三个不等式:
1. (3) x3 - x0 <= 8
2. (2) + (5) x3 - x0 <= 9
3. (1) + (4) + (5) x3 - x0 <= 7
这里的T等于{8, 9, 7},所以min{ T } = 7,答案就是7。的确是7吗?我们再仔细看看,发现的确没有其它情况了。那么问题就是这种方法即使做出来了还是带有问号的,不能确定正确与否,如何系统地解决这类问题呢?
2、最短路
让我们来看另一个问题,这个问题描述相对简单,给定四个小岛以及小岛之间的有向距离,问从第0个岛到第3个岛的最短距离。如图一-1-2所示,箭头指向的线段代表两个小岛之间的有向边,蓝色数字代表距离权值。
如图:
这个问题就是经典的最短路问题。由于这个图比较简单,我们可以枚举所有的路线,发现总共三条路线,如下:
1. 0 -> 3 长度为8
2. 0 -> 2 -> 3 长度为7+2 = 9
3. 0 -> 1 -> 2 -> 3 长度为2 + 3 + 2 = 7
最短路为三条线路中的长度的最小值即7,所以最短路的长度就是7。这和上面的不等式有什么关系呢?还是先来看看最短路求解的原理,看懂原理自然就能想到两者的联系了。但就算知道了也干不起题
差分约束系统的每一个约束条件Xi-Xj≤Ck可以变形为Xi≤Xj+Ck。这与单源最短路中的bellman—ford算法中的三角形不等式dis[v]≤dis[u]+w(u,v)非常相似。因此,可以把每个变量Xi看做有向图中的一个节点i,对于每一个约束条件Xi-Xj≤Ck,从节点j向节点i连一条长度为Ck的边。
而由于查分约束的原式是n元一次不等式,从数学上可以推到——差分约束系统的解有三种情况:1、有解;2、无解;3、无限多解。
1、我们在学习最短路的时候,会出现负权圈或者根本就不可达的情况,所以在不等式组转化的图上也有可能出现上述情况,先来看负权圈的情况,如图,下图为5个变量5个不等式转化后的图,需要求得是X[t] - X[s]的最大值,可以转化成求s到t的最短路,但是路径中出现负权圈,则表示最短路无限小,即不存在最短路,那么在不等式上的表现即X[t] - X[s] <= T中的T无限小,得出的结论就是 X[t] - X[s]的最大值 不存在。
2、 再来看另一种情况,即从起点s无法到达t的情况,如图,表明X[t]和X[s]之间并没有约束关系,这种情况下X[t] - X[s]的最大值是无限大,这就表明了X[t]和X[s]的取值有无限多种。
需要注意的是,如果{A1,A2,A3,·······,An}是一组解,那么对于任意的常数d,{A1+d,A2+d,A3+d,······,An+d}一定也是一组解(这个性质具体有什么用会在后面的例题中说道)
并且,在某些题目中,约束条件形如Xi-Xj≥Ck。我们还是可以看成从i到j有一条长度为Ck的有向边,只是改为计算单源最长路,若存在正环则无解。当然,我们也可以把不等式变为Xj-Xi≤﹣Ck,再进行如上的差分约束。
如果给出的不等式有"<=“也有”>=",又该如何解决呢?很明显,首先需要关注最后的问题是什么,如果需要求的是两个变量差的最大值,那么需要将所有不等式转变成"<=“的形式,建图后求最短路;相反,如果需要求的是两个变量差的最小值,那么需要将所有不等式转化成”>=",建图后求最长路。
如果有形如:A - B = c 这样的等式呢?我们可以将它转化成以下两个不等式:
A - B >= c (1)
A - B <= c (2)
再通过上面的方法将其中一种不等号反向,建图即可。
最后,如果这些变量都是整数域上的,那么遇到A - B < c这样的不带等号的不等式,我们需要将它转化成"<=“或者”>="的形式,即 A - B <= c - 1。
【例题一】(区间约束)
题目描述
一条街的一边有几座房子。因为环保原因居民想要在路边种些树。路边的地区被分割成块,并被编号成1…N。每个部分为一个单位尺寸大小并最多可种一棵树。每个居民想在门前种些树并指定了三个号码B,E,T。这三个数表示该居民想在B和E之间最少种T棵树。当然,B≤E,居民必须记住在指定区不能种多于区域地块数的树,所以T≤E-B+l。居民们想种树的各自区域可以交叉。你的任务是求出能满足所有要求的最少的树的数量。
写一个程序完成以下工作:
输入格式
第一行包含数据N,区域的个数(0<N≤30000);
第二行包含H,房子的数目(0<H≤5000);
下面的H行描述居民们的需要:B E T,0<B≤E≤30000,T≤E-B+1。
输出格式
输出文件只有一行写有树的数目
输入输出样例
输入
9
4
1 4 2
4 6 2
8 9 2
3 5 2
输出
5
本题思路
这题并不能直接套上面所说的算法,但是可以转换成上面的形式:
[l, r]至少要种T棵树 可以转换为Sum® - Sum(l-1) >= T
(sum_i是前缀和,即从1到i有多少棵树)
答案倒不是求这个前缀和序列(没意义),只要dis(N)
转换完,就可以用上面的方法求解了:
先乘-1,转换成Sum(l-1) - Sum® <= -T
于是r向l-1连一条长度为-T的边(明白前缀和就知道为什么是l-1而不是l了)
查看之前关于源点的约定,放到这里意思是:Sum(i) - Sum(0) <= 0
显然是错的,那就把N+1当作源点,把所有结点连到N+1,就行了
还有两个条件,即:
Sum(i) - Sum(i-1) <= 1
Sum(i-1) - Sum(i) <= 0
也要按照这两个条件加边
差分约束规定的只是元素的相对关系,按照题意 相对关系不变时最后的答案尽可能小
因此答案是dis(N) - min_dis,(min_dis是0~N最小的dis)也就是让min_dis=0(不种树)
举个例子好理解一点.比如N=4,下面是dis数组0~N
2, 2, 3, 5, 6
让它们都减去2,相对关系不变(就是上面提到过的那个性质)
0, 0, 1, 3, 4
这才是最优的解.
(dis有可能跑成负数,也不影响最后答案)
dis(N)是6-2=4,所以之前的Sum(N)就是4,答案也就是4了.
代码实现:
#include <iostream> #include <queue> #include <cstring> using namespace std; #define MAXN 30010 #define MAXM 100010 struct Edge { int next, to, w; } edge[MAXM]; int head[MAXN], cnt; void Add(int u, int v, int w) { edge[++cnt].w = w; edge[cnt].next = head[u]; head[u] = cnt; edge[cnt].to = v; } //用链式前向星存图 int N, M; int dis[MAXN]; bool vis[MAXN]; void SPFA(int S) //SPFA模版 { queue<int> Q; Q.push(S); for(int i=0; i<=N+1; i++) dis[i] = 1e9; dis[S] = 0; vis[S] = 1; while(!Q.empty()) { int k = Q.front(); Q.pop(); vis[k] = 0; for(int i=head[k]; i!=-1; i = edge[i].next) { int to = edge[i].to; if(dis[to] > dis[k] + edge[i].w) { dis[to] = dis[k] + edge[i].w; if(!vis[to]) { Q.push(to); vis[to] = 1; } } } } } int main() { int x, y, t, S; cin >> N >> M; S = N+1; //源点为N+1 memset(head, -1, sizeof(head)); for(int i=0; i<=N; i++) Add(S, i, 0); //Sum(i) - Sum(N+1) <= 0 for(int i=1; i<=M; i++) { cin >> x >> y >> t; Add(y, x-1, -t); //Sum(x-1) - Sum(y) <= -t } for(int i=1; i<=N; i++) { Add(i-1, i, 1); //Sum(i) - Sum(i-1) <= 1 Add(i, i-1, 0); //Sum(i-1) - Sum(i) <= 0 } SPFA(S); int _min = 1e9; for(int i=0; i<=N; i++) _min = min(_min, dis[i]); cout << dis[N] - _min << endl; return 0; }
提高题目:照片(也是很基础的 )
【例题二】(线性约束)
题目描述
德黑兰的一家超市每天24小时营业,需要一些收银员才能满足其需求。超市经理雇用你来帮助他,解决他的问题。问题是超市在每天的不同时间需要不同数量的收银员(例如,午夜后的几个收银员,下午的许多收银员)以便为其客户提供良好的服务,并且他希望雇用最少数量的收银员。这个工作的收银员。
经理为您提供了当天每一小时所需的最少数量的收银员。该数据以R(0),R(1),…,R(23)给出:R(0)表示从午夜到凌晨1:00需要的最少收银员数量,R(1)显示此数字持续时间为凌晨1:00至凌晨2:00,依此类推。请注意,这些数字每天都相同。这项工作有N名合格的申请人。每个申请人我每24小时不停地工作一次从指定的小时开始正好8小时,例如ti(0 <= ti <= 23),恰好是从所提到的小时开始。也就是说,如果第i个申请人被雇用,他/她将从时间开始工作8小时。收银员不会互相替换,并且按照计划完成工作,并且有足够的收银机和柜台供雇用的人使用。
您要编写一个程序来读取R(i)的i = 0 … 23和ti的i = 1 … N,它们都是非负整数并计算最小数量需要雇用收银员来满足上述限制。请注意,可以有比特定插槽所需的最少数量更多的收银员。
输入格式
第一行输入是此问题的测试用例数(最多20个)。每个测试用例以24个整数开始,表示一行中的R(0),R(1),…,R(23)(R(i)可以是至多1000)。然后在另一行中有N个申请人数(0 <= N <= 1000),之后是N行,每行包含一个ti(0 <= ti <= 23)。测试用例之间没有空行。
输出格式
对于每个测试用例,输出应写在一行中,这是所需的收银员数量最少。
如果测试用例没有解决方案,则应该针对该情况编写无解决方案。
样本输入
1
1 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
5
0
23
22
1
10
样本输出
1
解题思路
首先我们根据题意很容易 想到如下的不等式模型:
num[i]为i时刻能够开始工作的人数,x[i]为实际雇佣的人数,那么就有x[i]≤num[i]。
设r[i]为i时刻至少需要工作的人数,而每个员工可以工作8小时(我也要每天只工作8小时,55~),就可以推出:x[i-7]+x[i-6]+x[i-5]+x[i-4]+x[i-3]+x[i-2]+x[i-1]+x[i]≥r[i]
然后利用前缀和来优化算,设s[i]=x[1]+x[2]+x[3]+······+x[i],由此可以推到:
0≤s[i]-s[i-1]≤num[i],0 ≤ i ≤ 23,
s[i]-s[i-8]≥r[i],8 ≤ i ≤ 23,
s[23]-s[i+16]+s[i]≥r[i],0≤i≤7 (隔夜的情况)
由于最后这个表达式并不是标准的表达式,我们就可以把它整理成:s[i]-s[i+16]≥r[i]-s[23]
然后建图,枚举s[23]即可。
代码如下:
#include <bits/stdc++.h> using namespace std; #define maxn 300000 int cnp = 1, head[maxn], num[maxn]; int R[maxn], dis[maxn], in[maxn]; bool vis[maxn]; struct edge { int to, last, co; }E[maxn]; int read() { int x = 0, k = 1; char c; c = getchar(); while(c < '0' || c > '9') { if(c == '-') k = -1; c = getchar(); } while(c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar(); return x * k; } void add(int u, int v, int w) { E[cnp].to = v, E[cnp].co = w; E[cnp].last = head[u], head[u] = cnp ++; } bool spfa() { queue <int> q; for(int i = 1; i <= 30; i ++) dis[i] = -99999999; memset(vis, 0, sizeof(vis)); memset(in, 0, sizeof(in)); dis[0] = 0, q.push(0); while(!q.empty()) { int u = q.front(); q.pop(); vis[u] = 0; for(int i = head[u]; i; i = E[i].last) { int v = E[i].to; if(dis[v] < dis[u] + E[i].co) { dis[v] = dis[u] + E[i].co; if(!vis[v]) { if(++ in[v] == 26) return 1; vis[v] = 1; q.push(v); } } } } return 0; } void Build(int mid) { memset(head, 0, sizeof(head)); cnp = 1; for(int i = 1; i <= 24; i ++) { add(i - 1, i, 0); add(i, i - 1, -num[i]); } for(int i = 8; i <= 24; i ++) add(i - 8, i, R[i]); for(int i = 1; i < 8; i ++) add(i + 16, i, R[i] - mid); add(0, 24, mid); } int main() { int T = read(); while(T --) { for(int i = 1; i <= 24; i ++) R[i] = read(); int n = read(); memset(num, 0, sizeof(num)); for(int i = 1; i <= n; i ++) { int x = read(); num[x + 1] ++; } Build(n); if(spfa()) { printf("No Solution\n"); continue; } int l = 0, r = n, ans; while(l <= r) { int mid = (l + r) >> 1; Build(mid); if(spfa()) l = mid + 1; else ans = mid, r = mid - 1; } printf("%d\n", ans); } return 0; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。