差分约束系统学习笔记

躺在前面

不得不说厉害的人真的很多,有些题不看题解全靠me自己做真的太难了。me还是不够强啊。


差分是个什么呢?

差分就是,题目给定一堆要求,这些要求在转化后可以表示成多个A - B ≥ val 或者A - B ≤ val 组合起来的不等式组
构成这个不等式组的不等式,一般左边是两个状态(两个待求值),右边是一个定值(一般是已知值),然后再通过模型转化,转化成最长路或者最短路问题求解


举个栗子

给定一个长度为N的序列
题目将会以一些三元组[a,b,c]对这个数列进行描述,表示序列中数值在[a,b]之间的数字至少有ci个
询问使得所有条件都成立的序列长度最短是多少。

这个破题,让me看看啊…
先要把题目条件表示成式子
定义S数组:S[i]表示序列中数值在0到i之间的数的个数。
定义maxN:maxN为所有b中最大的,即整个数列的最大数值

那么题目中的限制条件就可以被表示成

S[b]S[a1]c S [ b ] − S [ a − 1 ] ≥ c

对不等式进行移项:
S[b]c+S[a1] S [ b ] ≥ c + S [ a − 1 ]

减一是因为[a,b]是闭区间,a也要包括在内。

而我们在跑最长路的时候,更新dist数组的条件是这样的

if(dist[v]<dist[u]+length)dist[v]=dist[u]+length i f ( d i s t [ v ] < d i s t [ u ] + l e n g t h ) d i s t [ v ] = d i s t [ u ] + l e n g t h

是不是有一点意思qwq
我们把这个S数组看成dist数组,把不等式看成边,一开始全部赋值为0,然后跑最长路。可以发现,本来不成立的不等式,在更新为最长路后,恰好满足了条件最苛刻的不等式,也就是取得了符合所有不等式的最小解。

回到题目上,把所有的约束条件列出来

for each limit :S[b]S[a1]cfor each i :S[i]S[i1]0for each i :S[i1]S[i]1 f o r   e a c h   l i m i t   : S [ b ] − S [ a − 1 ] ≥ c f o r   e a c h   i   : S [ i ] − S [ i − 1 ] ≥ 0 f o r   e a c h   i   : S [ i − 1 ] − S [ i ] ≥ − 1

将所有不等式全部变成B ≥ A + C的形式,按照从A连向B,边长为C的方式建图,跑最长路。这样跑出来的dist[S[maxN]]就是符合所有限制条件的最小解。

这道题是:POJ1201 可以练练手
代码如下
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;

int N , dist[50005] , head[50005] , tp , maxt ;
struct Path{
    int pre , to , len ;
}p[200005] ;

void In( int t1 , int t2 , int t3 ){
    p[++tp].pre = head[t1] ; 
    p[head[t1] = tp].to = t2 ;
    p[tp].len = t3 ;
}

int que[5000005] , ba , fr ;
bool inque[50005] ;
void SPFA(){
    memset( dist + 1, -0x3f , ( maxt + 1 ) * sizeof( int ) ) ;
    ba = 2500000 ; fr = 2500001 ;
    que[++ba] = 0 ; inque[0] = true ;
    while( fr <= ba ){
        int u = que[fr++] ; inque[u] = false ;
        for( int i = head[u] ; i ; i = p[i].pre ){
            int v = p[i].to , len = p[i].len ;
            if( dist[v] < dist[u] + len ){
                dist[v] = dist[u] + len ;
                if( !inque[v] ){
                    if( dist[v] >= dist[que[fr]] )
                        que[--fr] = v ;
                    else que[++ba] = v ;
                    inque[v] = true ;
                }
            }
        }
    }
    printf( "%d" , dist[maxt] ) ;
}

int main(){
    int t1 , t2 , t3 ;
    scanf( "%d" , &N ) ;
    for( int i = 1 ; i <= N ; i ++ ){
        scanf( "%d%d%d" , &t1 , &t2 , &t3 ) ;
        In( t1 - 1 , t2 , t3 ) ; maxt = max( maxt , t2 ) ;
    }
    for( int i = 0 ; i < maxt ; i ++ ){
        In( i , i + 1 , 0 ) ;
        In( i+1 , i , -1 ) ;
    }
    SPFA() ;
    return 0 ;
}
注:me的Bellman-Ford(该算法队列优化俗称SPFA)均使用双端队列优化,这样写在很多情况下会快不少,有些情况下会变慢(只遇到过一次)…有兴趣的同学也可以试试stack版的Bellman-Ford,这个有时候也会快,但是慢的时候占绝大多数
注2:其实不是很喜欢SPFA这个称呼,明明就是队列优化= =

一些其他的题

POJ2983

题意

给你一堆限制条件,这些限制条件分成两类,确定的(用P表示)和不确定的(用V)表示
[P a b c]表示:a-b=c
[V a b]表示:a-b≥1
若这些条件没有冲突,输出Reliable,不然输出Unreliable

这破题怎么做?

其实难点在于取等,差分约束只能解决大于等于符号,而无法解决等于符号。于是需要将等式用不等式表示如下

A+B=C{A+BCA+BC A + B = C ⇔ { A + B ≥ C A + B ≤ C

这两个不等式如果都要成立,相当于只能取等
建边这样建:
不等式:a到b,长度为1
等式:a到b,长度为val。b到a,长度为-val
直接跑最短路判断有没有负环就好了

代码

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;

int N , M , head[1005] , dist[1005] , tp ;
int que[5000005] , fr , ba , cnt[1005] ;
bool inque[1005] ;
struct Path{
    int pre , to , len ;
}p[200005] ;

void clear(){
    tp = 0 ;
    memset( head , 0 , sizeof( head ) ) ;
    memset( cnt , 0 , sizeof( cnt ) ) ;
    memset( dist , 0x3f , sizeof( dist ) ) ;
}

void In( int t1 , int t2 , int t3 ){
    p[++tp].pre = head[t1] ; 
    p[ head[t1] = tp ].to = t2 ;
    p[tp].len = t3 ;
}

bool SPFA(){
    fr = 2500001 ; ba = 2500000 ;
    for( int i = 1 ; i <= N ; i ++ )
        que[++ba] = i , inque[i] = true ;
    while( fr <= ba ){
        int u = que[fr++] ; inque[u] = false ;
        for( int i = head[u] ; i ; i = p[i].pre ){
            int v = p[i].to , len = p[i].len ;
            if( dist[v] > dist[u] + len ){
                dist[v] = dist[u] + len ;
                if( !inque[v] ){
                    if( dist[v] < dist[que[fr]] ) que[--fr] = v ;
                    else que[++ba] = v ;
                }
                inque[v] = true ; ++ cnt[v] ;
                if( cnt[v] > N + 2 ) return false ; 
            }
        }
    }
    return true ;
}

int main(){
    while( scanf( "%d%d" , &N , &M ) != EOF ){
        clear() ;
        char opt ;
        int a , b , x ;
        for( int i = 1 ; i <= M ; i ++ ){
            opt = getchar() ;
            while( opt != 'P' && opt != 'V' ) opt = getchar() ;
            switch ( opt ){
                case 'P' :
                    scanf( "%d%d%d" , &a , &b , &x ) ;
                    In( a , b , x ) ;
                    In( b , a , -x ) ;
                    break ;
                case 'V' :
                    scanf( "%d%d" , &a , &b ) ;
                    In( b , a , -1 ) ;
            }
        }
        puts( SPFA() ? "Reliable" : "Unreliable" ) ;
    }   
}

POJ3159

题意

一群小屁孩分糖果,他们的编号为1到N。要求编号为Bi小孩子分得的糖果不能超过编号为Ai的小孩子Ci个,不然他们就会不满意(= =给糖还不满意,不如给me这个穷得吃土的可怜孩子qwq)
询问在所有小孩都满意的情况下,N号小孩子最多比1号小孩子多几个糖果

Emmmmm,注意有多组数据。。。

题解

这题其实和普通的差分区别不大,只是这个题要跑最短路
如果不能理解的话,我们把这个题的题面转换一下:数轴上有一堆点,已知Bi点所在位置不大于Ai所在位置+Ci,询问N号点位置最远是多少。相当于是求出符合条件N的最大值。
条件写成式子:

Ai+CiBi A i + C i ≤ B i

按照从A向B,长度为C的方式建边。初始化dist数组为无限大然后跑最短路,那么这条最短路一定符合所有限制条件(符合所有上述式子)
之后就是常规建图跑了

代码

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;

int N , M , head[30005] , tp ;
struct Path{
    int to , pre , len ;
}p[150005];

void In( int t1 , int t2 , int t3 ){
    p[++tp].pre = head[t1] ;
    p[ head[t1] = tp ].to = t2 ;
    p[tp].len = t3 ;
}

int que[25000005] , dist[30005] , fr , ba ;
bool inque[30005] ;
void SPFA(){
    fr = 12500001 ; ba = 12500000 ;
    memset( dist , 0x3f , ( N + 1 ) * sizeof( int ) ) ;
    que[++ba] = 1 ; inque[1] = true ; dist[1] = 0 ;
    while( fr <= ba ){
        int u = que[fr++] ; inque[u] = false ;
        for( int i = head[u] ; i ; i = p[i].pre ){
            int v = p[i].to , len = p[i].len ;
            if( dist[v] > dist[u] + len ) {
                dist[v] = dist[u] + len ;
                if( !inque[v] ){
                    if( dist[v] <= dist[que[fr]] + 10 ) que[--fr] = v ;
                    else que[++ba] = v ;
                }
                inque[v] = true ;
            }
        }
    }
}

int main(){
    int a , b , c ;
    while( scanf( "%d%d" , &N , &M ) != EOF ){
        memset( head , 0 , sizeof( head ) ) ;
        tp = 0 ;
        for( register int i = 1 ; i <= M ; i ++ ){
            scanf( "%d%d%d" , &a , &b , &c ) ;
            In( a , b , c ) ;
        }
        SPFA() ;
        printf( "%d\n" , dist[N] ) ;
    }
}

POJ1364

题意

已知一个序列a[1], a[2], ……, a[n],给出一些对子串的限制,每个限制形如
[s n gt/lt k](gt表示大于,lt表示小于),这表示:

{i=ss+na[i]}<=k { ∑ i = s s + n a [ i ] } <= k

询问是否有满足所有限制条件的序列

题解

这题,其实不算很难想的
题目上的限制都是针对一段子串的和,明显是不能直接把式子写成像这样的:

a[s]+a[s+1]+a[s+2]++a[s+n]k a [ s ] + a [ s + 1 ] + a [ s + 2 ] + · · · + a [ s + n ] ≤ k

因为这样是不符合A-B≤val的形式,未知数超过了两个,无法建边
于是考虑把多个值相加转换成前缀和相减

定义S数组:S[i]为a[1]到a[i]的和

那么上面的式子可以表示为:

S[s+n]S[s1]k S [ s + n ] − S [ s − 1 ] ≤ k

剩下的步骤和普通题都差不多了,几乎就是上面讲到的POJ1201照搬就可以

代码

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;

int N , M , tp , head[105] , dist[105] , cnt[105] ;
struct Path{
    int pre , to , len ;
}p[205] ;

void clear(){
    tp = 0 ;
    memset( head , 0 , sizeof( head ) ) ;
    memset( dist , 0x3f , sizeof( dist ) ) ;
    memset( cnt , 0 , sizeof( cnt ) ) ;
}

void In( int t1 , int t2 , int t3 ){
    p[++tp].pre = head[t1] ;
    p[ head[t1] = tp ].to = t2 ;
    p[tp].len = t3 ;
}

bool inque[105] ;
int que[50005] , fr , ba ;
bool SPFA(){
    fr = 25001 ; ba = 25000 ;
    for( int i = 0 ; i <= N ; i ++ )//注意是前缀和,从S[0]开始计算,0也要push进去 
        que[++ba] = i , inque[i] = true ;
    while( fr <= ba ){
        int u = que[fr++] ; inque[u] = false ;
        for( int i = head[u] ; i ; i = p[i].pre ){
            int v = p[i].to , len = p[i].len ;
            if( dist[v] > dist[u] + len ){
                dist[v] = dist[u] + len ;
                if( !inque[v] ){
                    if( dist[v] <= dist[que[fr]] ) que[--fr] = v ;
                    else que[++ba] = v ;
                }
                inque[v] = true ; cnt[v] ++ ;
                if( cnt[v] > N ) return false ;
            }
        }
    }
    return true ;
}

int main(){
    int a , b , Val ;
    char ss[5] ;
    while( scanf( "%d" , &N ) && N && scanf( "%d" , &M) ){
        clear() ;
        for( int i = 1 ; i <= M ; i ++ ){
            scanf( "%d%d%s%d" , &a , &b , ss , &Val ) ;
            switch ( ss[0] ){
                case 'g' : In( a+b , a-1 , -Val-1 ) ;break ;
                case 'l' : In( a-1 , a+b , Val-1 ) ;break ;
            }
        }
        puts( SPFA() ? "lamentable kingdom" : "successful conspiracy" ) ;
    }
    return 0 ;
}

POJ1275

说在前面

这个题可以说是差分题当中相当难的,需要转换未知量,但是写起来并不难,是道很不错的题,推荐写一写

题意

有一家全天营业的超市,这家超市在每天的不同时段需要不同数目的出纳员(例如,午夜只需一小批,而下午则需要很多)来为顾客提供优质服务,超市经理希望雇佣最少数目的纳员。
超市经理会提供一天里每一小时需要出纳员的最少数量:R(0),R(1),…,R(23)。R(0)表示从午夜到凌晨1:00所需要出纳员的最少数目(可以比这个数目多),R(1)表示凌晨1:00到2:00之间需要的,以此类推。
现在有N人申请这项工作,每个申请者i都会从一个特定的时刻开始连续工作恰好8小时。定义ti(0<=ti<=23)为上面提到的开始时刻,也就是说,如果第i个申请者被录用,他(或她)将从ti时刻开始连续工作8小时。
计算为满足上述限制需要雇佣的最少出纳员数目。

题解

说实话,这个题,不是很想写题解,写起来能写一小时···
要注意的地方实在很多。
就在这里引用一下别人的题解吧
hr_whisper的POJ1275题解

大牛解释如下:
为避免负数,时间计数1~24。令:
R[i] i时间需要的人数 (1<=i<=24)
T[i] i时间应聘的人数 (1<=i<=24)
x[i] i时间录用的人数 (0<=i<=24),其中令x[0]=0
再设s[i]=x[0]+x[1]+……+x[i] (0<=i<=24),
由题意,可得如下方程组:
(1) s[i]-s[i-8]>=R[i] (8<=i<=24)
(2) s[i]-s[16+i]>=R[i]-s[24] (1<=i<=7)
(3) s[i]-s[i-1]>=0 (1<=i<=24)
(4) s[i-1]-s[i]>=-T[i] (1<=i<=24)

这个差分约束有个特殊的地方,(2)的右边有未知数s[24]。
这时可以通过枚举s[24]=ans来判断是否有可行解。
即(2)变形为(2’) s[i]-s[16+i]>=R[i]-ans (1<=i<=7)
再通过SPFA求解(1)(2’)(3)(4)。

不过最后有可能出现这种情况:
(1)(2’)(3)(4)虽然有解,但求出的s[24]小于代入(2’)里的ans!
这时,显然得到的s[]不满足原来的(2)了(请仔细比较(2)与(2’))。
不过虽然得到的解不满足原方程组,但这并不代表(1)(2)(3)(4)在s[24]=ans时没有可行解!
此外,值得注意的是,当得到的s[24]>ans时,虽然s[24]不一定是最优解,但把ans置成s[24]后,确实是可行解。

所以,简单把(2)置换成(2’)是有问题的!
为了等价原命题,必须再加上条件:s[24]>=ans
这就是所谓加出来的那条边(5) s[24]-s[0]>=ans

代码

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;

int Tot , N , R[30] , T[30] , head[30] , tp , ans , dist[30] ;
struct Path{
    int pre , to , len ;
}p[200] ;

void In( int t1 , int t2 , int t3 ){
    p[++tp].pre = head[t1] ;
    p[ head[t1] = tp ].to = t2 ;
    p[tp].len = t3 ;
}

void addE( int x ){
    tp = 0 ;
    memset( head , 0 , sizeof( head ) ) ; 
    for( int i = 1 ; i <= 24 ; i ++ ){
        In( i - 1 , i , 0 ) ;
        In( i , i - 1 , -T[i] ) ;
    }   
    for( int i = 8 ; i <= 24 ; i ++ )
        In( i - 8 , i , R[i] ) ;
    for( int i = 1 ; i <= 7 ; i ++ )
        In( 16 + i , i , R[i] - x ) ;
    In( 0 , 24 , x ) ;
}

bool inque[25] ;
int que[50005] , fr , ba , cnt[25] ;
bool SPFA( int x ){
    memset( dist , ~0x3f , sizeof( dist ) ) ;
    memset( cnt , 0 , sizeof( cnt ) ) ;
    memset( inque , 0 , sizeof( inque ) ) ;
    fr = 25001 ; ba = 25000 ;
    que[++ba] = 0 ; dist[0] = 0 ; inque[0] = true ;
    while( fr <= ba ){
        int u = que[fr++] ; inque[u] = false ;
        for( int i = head[u] ; i ; i = p[i].pre ){
            int v = p[i].to , len = p[i].len ;
            if( dist[v] < dist[u] + len ){
                dist[v] = dist[u] + len ;
                if( !inque[v] ){
                    if( dist[v] > dist[que[fr]] ) que[--fr] = v ;
                    else que[++ba] = v ;
                    inque[v] = true ; ++ cnt[v] ;
                    if( cnt[v] > 25 ) return false ;
                }
            }
        }
    }
    if( dist[24] != x ) return false ;
    return true ;
}

void solve(){
    int lf = 0 , rg = N ; ans = -1 ;
    while( lf <= rg ){
        int mid = ( lf + rg ) >> 1 ;
        addE( mid ) ;
        if( SPFA( mid ) ){
            rg = mid - 1 ;
            ans = mid ;
        } else lf = mid + 1 ;
    }
}

int main(){
    scanf( "%d" , &Tot ) ;
    while( Tot-- ){
        for( int i = 1 ; i <= 24 ; i ++ )
            scanf( "%d" , &R[i] ) ;
        scanf( "%d" , &N ) ;
        for( int i = 1 , j ; i <= N ; i ++ )
            scanf( "%d" , &j ) , T[j+1] ++ ;
        solve() ;
        printf( ans == -1 ? "No Solution\n" : "%d\n" , ans ) ;  
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值