【算法提高课】图论:差分约束


title: 【算法提高课】图论:SPFA找负环
katex: true
tags:

  • Acwing
  • medium
  • 图论
    categories: 算法提高课

概论

  • 求不等式组的可行解
    • 需要满足的条件: 从源点出发,一定可以走到所有的边
    • 步骤(以最短路为例):
      1. 先将每个不等式 x i ≤ x j + c k x_i \leq x_j+c_k xixj+ck 转化成一条从 x j x_j xj 走到 x i x_i xi ,长度为 c k c_k ck 的一条边
      2. 找一个超级源点,使得该源点一定可以遍历到所有的边
      3. 从源点求一遍单源最短路
        结果1:如果存在负环,那么原不等式一定无解
        结果2: 如果没有负环,那么dis[i]就是原不等式的一个可行解
  • 如何求最大值或最小值(这里的最值指的是每个变量的最值:
    • 结论: 如果求的是最小值,应求最长路。如果求最大值,应求最短路。举例理解dis[j]<dis[t]+w[i] 这是最短路符合的关系式,那左边的是不是很像最大值?
    • 问题: 如何转化 x i ≤ c x_i \leq c xic,其中 c c c 为常数,这类的不等式
    • 方法: 建立一个超级源点, 0 0 0,然后建立 0 − > i 0->i 0>i ,长度为 c c c 的边即可
      以求 x i x_i xi 的最大值为例,求所有从 x i x_i xi 出发,构成的不等式链 x i ≤ x j + c 1 ≤ x k + c 2 + c 1 ≤ . . . ≤ c 1 + c 2 + c 3 . . . + c i x_i \leq x_j+c_1 \leq x_k+c_2+c_1 \leq ... \leq c_1+c_2+c_3...+c_i xixj+c1xk+c2+c1...c1+c2+c3...+ci 所计算出的上界,最终 x i x_i xi 的最大值等于所有上界的最小值(取并集
  • 注意: 差分约束的链式法则在不等式链的末端一定会是一个常数

Acwing.1169 糖果

  • 题意: N ( 1 e 5 ) N(1e5) N(1e5) 个小朋友,在分糖果时需要满足 K ( 1 e 5 ) K(1e5) K(1e5) 个要求,每个小朋友都至少要分到一个糖果,求满足要求所需的最小糖果数量。
    要求如下:
    • A 和 B 分到的一样多
    • A 比 B 多
    • A 不少于 B
    • B 不少于 A
    • B 比 A 多
  • 思路: 题目有一个个不等式:说明本题为差分约束。题目要求最少需要准备多少个糖果:对应差分约束的最小求最长路。那么只需要将题目的不等式转化即可:注意本题为最长路,和上面的最短路举例建边是相反的。简单的判断方法:b比a多所以b要在a后面(因为最长路)。
  • 雷点: 数据范围。本题用队列会 T。用栈则不会
  • C o d e Code Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
typedef long long ll;
int head[N],to[N],nxt[N],w[N],idx;
ll dis[N];
int cnt[N],n,m;
bool st[N];
ll res;

void add(int u,int v,int val){
   to[++idx]=v,w[idx]=val,nxt[idx]=head[u],head[u]=idx;
}

bool spfa(){
   memset(dis,-0x3f,sizeof dis);
   stack<int>stk;
   dis[0]=0;
   stk.push(0);
   st[0]=true;
   
   while(stk.size()){
       int t=stk.top();
       stk.pop();
       st[t]=false;
       
       for(int i=head[t];i;i=nxt[i]){
           int j=to[i];
           if(dis[j]<dis[t]+w[i]){
               dis[j]=dis[t]+w[i];
               cnt[j]=cnt[t]+1;
               if(cnt[j]>=n+1)   return false;
               if(!st[j]){
                   stk.push(j);
                   st[j]=true;
               }
           }
       }
   }
   return true;
}

int main(){
   cin>>n>>m;
   for(int i=1;i<=m;i++){
       int a,b,c;
       cin>>c>>a>>b;
       if(c==1)    add(a,b,0),add(b,a,0);
       else if(c==2)   add(a,b,1);
       else if(c==3)   add(b,a,0);
       else if(c==4)   add(b,a,1);
       else    add(a,b,0);
   }   
   
   for(int i=1;i<=n;i++)   add(0,i,1);
 //  spfa();
   if(!spfa()) puts("-1");
   else{
       for(int i=1;i<=n;i++){
           res+=dis[i];     
          // cout<<dis[i]<<endl;
       }
       cout<<res;
   }
   
   return 0;
}

Acwing.362 区间

  • 题意: 给定 n ( 5 e 4 ) n(5e4) n(5e4) 个区间 [ a i , b i ] [a_i,b_i] [ai,bi] n n n 个整数 c i c_i ci。你需你需要构造一个整数集合 Z
    ,使得 ∀ i ∈ [ 1 , n ] ∀i∈[1,n] i[1,n] Z Z Z中满足 a i ≤ x ≤ b i a_i≤x≤b_i aixbi的整数 x x x 不少于 c i c_i ci个。求这样的整数集合 Z Z Z 最少包含多少个数。
  • 思路: 考虑前缀和 S i S_i Si 表示区间 [ 1 , i ] [1,i] [1,i] 所包含的整数数量。由此我们可以得出下列不等式
    • S i ≥ S i − 1 S_i ≥ S_{i-1} SiSi1 :前缀和性质
    • S i − S i − 1 ≤ 1 S_i-S_{i-1}≤1 SiSi11:后一项比前一项最多大1
    • S b − S a − 1 ≥ c S_b-S_{a-1}≥c SbSa1c:区间 [ a , b ] [a,b] [a,b] 至少有 c c c 个数,前缀和思想
  • C o d e Code Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int idx,nxt[N],to[N],w[N],head[N];
int dis[N],n,m;
bool st[N];

void add(int u,int v,int val){
    to[++idx]=v,w[idx]=val,nxt[idx]=head[u],head[u]=idx;
}

void spfa(){
    queue<int>q;
    q.push(0);
    memset(dis,-0x3f,sizeof dis);
    dis[0]=0;
    
    while(q.size()){
        int t=q.front();
        q.pop();
        st[t]=false;
        
        for(int i=head[t];i;i=nxt[i]){
            int j=to[i];
            
            if(dis[j]<dis[t]+w[i]){
                dis[j]=dis[t]+w[i];
                
                if(!st[j]){
                    q.push(j);
                    st[j]=true;
                }
            }
        }
    }
}

int main(){
    cin>>n;
    for(int i=1;i<=50001;i++){
        add(i-1,i,0);
        add(i,i-1,-1);
    }
    for(int i=1;i<=n;i++){
        int a,b,c;
        cin>>a>>b>>c;
        a++,b++;
        add(a-1,b,c);
    }
    
    spfa();
    
    cout<<dis[50001];
    
    return 0;
}

Acwing.1170 排队布局

  • 题意: 给出 N ( 1000 ) N(1000) N(1000) 头牛,编号从 1 1 1 N N N ,牛在队伍的顺序和他们的编号是相同的,可能有多条奶牛站在一个位置上。想象奶牛站在一条数轴上,给定以下关系
    • 两只奶牛相对位置不小于 L L L
    • 两只奶牛相对位置不大于 D D D
      1 1 1 号奶牛到 N N N 号奶牛的最大距离,若不存在输出 − 1 -1 1,无限大输出 − 2 -2 2
  • 思路: 题目已经有两个不等式,而且隐含一个不等式:顺序和编号是相同的
  • C o d e Code Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
const int INF=0x3f3f3f3f;
int head[N],nxt[N],to[N],w[N],idx;
int cnt[N],dis[N];
bool st[N];
int n,m,M;

void add(int u,int v,int val){
    to[++idx]=v,nxt[idx]=head[u],w[idx]=val,head[u]=idx;
}

bool spfa(int pos){
    memset(cnt,0,sizeof cnt);
    memset(dis,0x3f,sizeof dis);
    memset(st,false,sizeof st);
    queue<int>q;
    for(int i=1;i<=pos;i++){
        q.push(i);
        dis[i]=0;
        st[i]=true;
    }
    
    while(q.size()){
        int t=q.front();
        q.pop();
        st[t]=false;
        
        for(int i=head[t];i;i=nxt[i]){
            int j=to[i];
            
            if(dis[j]>dis[t]+w[i]){
                dis[j]=dis[t]+w[i];
                cnt[j]=cnt[t]+1;
                if(cnt[j]>=n)   return true;
                if(!st[j]){
                    q.push(j);
                    st[j]=true;
                }
            }
        }
    }
    return false;
}

int main(){
    cin>>n>>m>>M;
    for(int i=1;i<n;i++){
        add(i+1,i,0);
    }
    while(m--){
        int a,b,c;
        cin>>a>>b>>c;
        if(a>b) swap(a,b);
        add(a,b,c);
    }
    while(M--){
        int a,b,c;
        cin>>a>>b>>c;
        if(a>b) swap(a,b);
        add(b,a,-c);
    }
    
    if(spfa(n))    puts("-1");
    else{
        spfa(1);
        if(dis[n]>INF/2)    puts("-2");
        else    cout<<dis[n]<<endl;;
    }
    
    
    return 0;
}

Acwing.393 雇佣收银员

  • 题意: 一家超市二十四小时营业,现给出 0 - 23 小时每一小时时间段所需的收银员个数,并给出 N N N 位收银员和他们的开始值班时间,每个收银员会值班八个小时,求最少需要雇佣多少名收银员
  • 思路: 和第二题类似,利用前缀和的思想维护信息,本题前缀和表示前多少小时招多少人。本题有一个特殊的点是数据是环形的,前缀和比较特殊。其中分类讨论过程中会出现一个 S 24 S_{24} S24 ,这个是我们要求的数,所以我们直接枚举即可
  • 偷懒:
  • 由于牵扯到 [ i − 8 + 1 , i ] [i - 8 + 1, i] [i8+1,i] 这段区间的和的约束,所以用前缀和更好表达一些。
    n u m [ i ] num[i] num[i] 表示 i i i 时刻有多少人申请上岗, x [ i ] x[i] x[i] i i i 时刻实际上岗的人数 , s s s x x x 的前缀和数组。

则应该满足的约束条件是:

上岗人数不能负数,即 s [ i ] − s [ i − 1 ] > ; = 0 s[i] - s[i - 1] \gt;= 0 s[i]s[i1]>;=0
实际上岗人数不能超过申请人数,即 s [ i ] − s [ i − 1 ] < = n u m [ i ] s[i] - s[i - 1] \lt= num[i] s[i]s[i1]<=num[i]
i i i 时刻所在人数,即 [ i − 7 , i ] [i - 7, i] [i7,i] 区间内的上岗人数要大于等于最小需求 R R R

由于存在环,即 23 23 23 24 24 24,再到 0 0 0 时刻,所以要分类讨论

KaTeX parse error: Expected 'EOF', got '&' at position 3: i &̲gt;= 8 时,KaTeX parse error: Expected 'EOF', got '&' at position 17: …[i] - s[i - 8] &̲gt;= R[i]
KaTeX parse error: Expected 'EOF', got '&' at position 3: i &̲lt;= 7 时, s [ i ] + s [ 24 ] − s [ 16 + i ] > = R [ i ] s[i] + s[24] - s[16 + i] \gt= R[i] s[i]+s[24]s[16+i]>=R[i]

显然这是一个明显的差分约束问题,由于求最小人数,所以用最长路转化:

s [ i ] > = s [ i − 1 ] s[i] \gt= s[i - 1] s[i]>=s[i1] a d d ( i − 1 , i , 0 ) add(i - 1, i, 0) add(i1,i,0)
s [ i ] − n u m [ i ] < = s [ i − 1 ] s[i] - num[i] \lt= s[i - 1] s[i]num[i]<=s[i1] a d d ( i , i − 1 , − n u m [ i ] ) add(i, i - 1, -num[i]) add(i,i1,num[i])
s [ i − 8 ] + R [ i ] < = s [ i ] s[i - 8] + R[i] \lt= s[i] s[i8]+R[i]<=s[i] a d d ( i − 8 , i , R [ i ] ) add(i - 8, i, R[i]) add(i8,i,R[i])
s [ 16 + i ] + R [ i ] − s [ 24 ] < = s [ i ] s[16 + i] + R[i] - s[24] \lt= s[i] s[16+i]+R[i]s[24]<=s[i],不会连边了hhhh

最后一种约束关系我们不会连边的原因无非是出现了三个变量,但我们可以发现:

所有最后一种约束关系都有 s [ 24 ] s[24] s[24] 变量,其实这个东西就是我们求的答案,所以我们可以枚举 s [ 24 ] s[24] s[24] 的值,把它变成常量就行啦!然后就可以 a d d ( 16 + i , i , R [ i ] − s [ 24 ] ) add(16 + i, i, R[i] - s[24]) add(16+i,i,R[i]s[24])

T i p s : Tips: Tips

关于建图,其实可以在线建图,不用僵化建边了嘿嘿。
发现 0 0 0 肯定所有点,所以不用创造超级源点了,只需从 0 0 0 点出发跑最短路即可。
不要忘了 $s[24] = $ 我们枚举的数 c c c(要严格等于,实现是 大于等于 + 小于等于):
KaTeX parse error: Expected 'EOF', got '&' at position 7: s[24] &̲lt;= c 即 $add(24, 0, -c)
KaTeX parse error: Expected 'EOF', got '&' at position 7: s[24] &̲gt;= c a d d ( 0 , 24 , c ) add(0, 24, c) add(0,24,c)

时间复杂度
这个题中的点数 $ <= 26$,边数 $ <= 26 * 3 = 78$,
所以时间复杂度 O ( T n 2028 ) O(Tn2028) O(Tn2028),足以 A C AC AC

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 30, M = 100, INF = 0x3f3f3f3f;

int n;
int h[N], e[M], w[M], ne[M], idx;
int r[N], num[N];
int dist[N];
int q[N], cnt[N];
bool st[N];

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

void build(int c)
{
    memset(h, -1, sizeof h);
    idx = 0;
    add(0, 24, c), add(24, 0, -c);
    for (int i = 1; i <= 7; i ++ ) add(i + 16, i, r[i] - c);
    for (int i = 8; i <= 24; i ++ ) add(i - 8, i, r[i]);
    for (int i = 1; i <= 24; i ++ )
    {
        add(i, i - 1, -num[i]);
        add(i - 1, i, 0);
    }
}

bool spfa(int c)
{
    build(c);

    memset(dist, -0x3f, sizeof dist);
    memset(cnt, 0, sizeof cnt);
    memset(st, 0, sizeof st);

    int hh = 0, tt = 1;
    dist[0] = 0;
    q[0] = 0;
    st[0] = true;

    while (hh != tt)
    {
        int t = q[hh ++ ];
        if (hh == N) hh = 0;
        st[t] = false;

        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (dist[j] < dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= 25) return false;
                if (!st[j])
                {
                    q[tt ++ ] = j;
                    if (tt == N) tt = 0;
                    st[j] = true;
                }
            }
        }
    }

    return true;
}

int main()
{
    int T;
    cin >> T;
    while (T -- )
    {
        for (int i = 1; i <= 24; i ++ ) cin >> r[i];
        cin >> n;
        memset(num, 0, sizeof num);
        for (int i = 0; i < n; i ++ )
        {
            int t;
            cin >> t;
            num[t + 1] ++ ;
        }

        bool success = false;
        for (int i = 0; i <= 1000; i ++ )
            if (spfa(i))
            {
                cout << i << endl;
                success = true;
                break;
            }

        if (!success) puts("No Solution");
    }

    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值