差分约束

差分约束是一种特殊的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;
}
  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值