差分约束

8 篇文章 0 订阅

一、概念
转自博客:http://www.cppblog.com/menjitianya/archive/2015/11/19/212292.html
给定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吗?我们再仔细看看,发现的确没有其它情况了。那么问题就是这种方法即使做出来了还是带有问号的,不能确定正确与否,如何系统地解决这类问题呢?
让我们来看另一个问题,这个问题描述相对简单,给定四个小岛以及小岛之间的有向距离,问从第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。这和上面的不等式有什么关系呢?还是先来看看最短路求解的原理,看懂原理自然就能想到两者的联系了。

二、差分约束
回到单个不等式上来,观察 x[i] - x[j] <= a[k], 将这个不等式稍稍变形,将x[j]移到不等式右边,则有x[i] <= x[j] + a[k],然后我们令a[k] = w(j, i),再将不等式中的i和j变量替换掉,i = v, j = u,将x数组的名字改成d(以上都是等价变换,不会改变原有不等式的性质),则原先的不等式变成了以下形式:d[u] + w(u, v) >= d[v]。
这时候联想到SPFA中的一个松弛操作:
if(d[u] + w(u, v) < d[v])
d[v] = d[u] + w(u, v);
对比上面的不等式,两个不等式的不等号正好相反,但是再仔细一想,其实它们的逻辑是一致的,因为SPFA的松弛操作是在满足小于的情况下进行松弛,力求达到d[u] + w(u, v) >= d[v],而我们之前令a[k] = w(j, i),所以我们可以将每个不等式转化成图上的有向边:
对于每个不等式 x[i] - x[j] <= a[k],对结点 j 和 i 建立一条 j -> i的有向边,边权为a[k],求x[n-1] - x[0] 的最大值就是求 0 到n-1的最短路。

差分约束系统的解法如下:
1、 根据条件把题意通过变量组表达出来得到不等式组,注意要发掘出隐含的不等式,比如说前后两个变量之间隐含的不等式关系,从而能使这样我们就构造出来了一系列边,但是这远远不够,因为有很多点依然没有相连接起来(也就是从起点可能根本就还没有到终点的路线)此时,如果我们能构造出一系列的边的话,这样从起点到终点的最短路也就顺理成章的出现了。
2、 进行建图:
首先根据题目的要求进行不等式组的标准化。
(1)、如果要求取最小值,那么求出最长路,那么将不等式全部化成xi – xj >= k的形式,这样建立j->i的边,权值为k的边,如果不等式组中有xi – xj > k,因为一般题目都是对整形变量的约束,化为xi – xj >= k+1即可,如果xi – xj = k呢,那么可以变为如下两个:xi – xj >= k, xi – xj <= k,进一步变为xj – xi >= -k,建立两条边即可。
(2)、如果求取的是最大值,那么求取最短路,将不等式全部化成xi – xj <= k的形式, 这样建立j->i的边,权值为k的边,如果像上面的两种情况,那么同样地标准化就行了。
(3)、如果要判断差分约束系统是否存在解,一般都是判断环,选择求最短路或者最长路求解都行,只是不等式标准化时候不同,判环地话,用spfa即可,n个点中如果同一个点入队超过n次,那么即存在环。
值得注意的一点是:建立的图可能不联通,我们只需要加入一个超级源点,比如说求取最长路时图不联通的话,我们只需要加入一个点S,对其他的每个点建立一条权值为0的边图就联通了,然后从S点开始进行spfa判环。最短路类似。
3、 建好图之后直接spfa或bellman-ford求解,不能用dijstra算法,因为一般存在负边,注意初始化的问题。

例题:
(1)、求取最小值
poj1201
题意:给定n(n <= 50000)个整点闭区间和这个区间中至少有多少整点需要被选中,每个区间的范围为[ai, bi],并且至少有ci个点需要被选中,其中0 <= ai <= bi <= 50000,问[0, 50000]至少需要有多少点被选中。
例如3 6 2 表示[3, 6]这个区间至少需要选择2个点,可以是3,4也可以是4,6(总情况有 C(4, 2)种 )。

设dis[i]为[0,i]中满足条件的点的个数,那么dis[bi]-dis[ai-1]>=c,仅有这些边不足以把所有点连起来,因此需要挖掘题目中的条件,因为对于一个点i只有取和不取两种状态,因此可得0<=dis[i]-dis[i-1]<=1,即dis[i-1]-dis[i]>=-1 , dis[i]-dis[i-1]>=0,建立了图以后,SPFA跑一遍最长路
数据量比较大,用链式前向星存图

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <queue>
#include <map>
#include<stack>
using namespace std;
#define ll long long
#define eps 0.001
#define INF 0x3f3f3f3f
#define PI acos(-1.0)
#define memset(a,b) memset(a,b,sizeof(a))
//用链式前向星存图
struct node
{
    int next;//下一条边的存储位置
    int e; //终点
    int v;  //权值
}edge[50005*3];
int dis[50005];//【0,i】区间内有多少个点被选中
bool vis[50005]; //在队列标志
int head[50005];//记录以i为起点的第一条边的存储位置
int cnt=0,maxx=0,minn=INF;
void add(int u,int v,int w)
{
    edge[cnt].e=v;
    edge[cnt].next=head[u];
    edge[cnt].v=w;
    head[u]=cnt++;
}
void SPFA()
{
    minn-=1;
    memset(vis,false);
    memset(dis,-INF);
    vis[minn]=true;
    dis[minn]=0;
    queue<int>que;
    que.push(minn);
    while(!que.empty())
    {
        int u=que.front();
        que.pop();
        vis[u]=false;
        for(int i=head[u];~i;i=edge[i].next)
        {
            int flag=edge[i].e;
            if(dis[flag]<dis[u]+edge[i].v)
            {
                dis[flag]=dis[u]+edge[i].v;
                if(!vis[flag])
                {
                    vis[flag]=true;
                    que.push(flag);
                }
            }
        }
    }

}
int main()
{
    int n;
    int a,b,c;
    memset(head,-1);
    scanf("%d",&n);
    for(int i=0;i<n;i++)
    {
        scanf("%d %d %d",&a,&b,&c);
        a+=1;
        b+=1;
        add(a-1,b,c);
        minn=min(a,minn);
        maxx=max(b,maxx);
    }
    for(int i=minn;i<maxx;i++)
    {
        add(i,i+1,0);
        add(i+1,i,-1);
    }
    SPFA();
    printf("%d\n",dis[maxx]);
    return 0;
}

(2)、求最大值
poj3169
题意:有n头牛,有的互相不喜欢因此他们的距离必须超过D,有的相互喜欢,因此他们的距离不能超过d,可能会有好多头牛挤在同一个位置上,问1到n的最大距离,如果不满足排列输出-1,距离无限大输出-2。

d[i]表示的是第i个牛的位置
对于相互不喜欢的牛,d[i]+D<=d[j] 变形后 d[i]-d[j]<=-D,建立j到i的权值为-D的边
对于相互喜欢的牛,d[i]+d>=d[j] 变形后 d[j]-d[i]<=d,建立i到j的权值为d的边
d[i]<=d[i+1]
建立图后跑一遍最短路求最大值

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <queue>
#include <map>
#include<stack>
using namespace std;
#define ll long long
#define eps 0.001
#define INF 0x3f3f3f3f
#define PI acos(-1.0)
#define memset(a,b) memset(a,b,sizeof(a))
struct node
{
    int e;
    int v;
};
int dis[1005];
int cnt[1005];   //每个点入队次数
bool vis[1005];  //在队列标志
vector<node>q[1005];
int SPFA(int n)
{
    memset(vis,false);
    vis[1]=true;
    dis[1]=0;
    queue<int>que;
    while(!que.empty())
        que.pop();
    que.push(1);
    memset(cnt,0);
    cnt[1]=1;
    while(!que.empty())
    {
        int u=que.front();     
        que.pop();
        vis[u]=false;    //从队列取出则取消标记
        for(int i=0; i<q[u].size(); i++)
        {
            int flag=q[u][i].e;
            if(dis[flag]>dis[u]+q[u][i].v)    //更新节点
            {
                dis[flag]=dis[u]+q[u][i].v;
                if(!vis[flag])
                {
                    vis[flag]=true;
                    que.push(flag);
                    if(++cnt[flag]>n)       //计算入队次数,如果大于n则存在环
                        return 0;
                }
            }
        }
    }
    return 1;
}
int main()
{
    int n,ml,md,a,b,d;
    cin>>n>>ml>>md;
    memset(dis,INF);
    for(int i=1; i<=n; i++)
        q[i].clear();
    for(int i=0; i<ml; i++)
    {
        cin>>a>>b>>d;
        q[a].push_back(node{b,d});
    }
    for(int i=0; i<md; i++)
    {
        cin>>a>>b>>d;
        q[b].push_back(node{a,-d});
    }
    for(int i=1; i<n; i++)
        q[i+1].push_back(node{i,0});
    int ans=SPFA(n);
    if(ans==0)
        printf("-1\n");
    else
    {
        if(dis[n]!=INF)
            printf("%d\n",dis[n]);
        else
            printf("-2\n");
    }

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值