【圖論】差分約束系統

本文主要是記錄差分約束系統練習題的思路和代碼,在此不再詳細解釋算法原理。

差分約束系統原理,參考文章:https://blog.csdn.net/dragon60066/article/details/80245797

POJ 1275 Cashier Employment

思路:

設 num[i] 爲來應聘的在第i個小時開始工作的人數,
   r[i] 爲第i個小時至少需要的人數,
   x[i] 爲招到的在第i個小時開始工作的人數,
   x[i] 的前綴和爲 s[i] ;
   爲了防止下標出現-1,將0~23右移一位,之後用1~24表示時刻。
   根據題意有:
         0 <= x[i] <= num[i] 即 0<=s[i]-s[i-1]<=num[i]
         x[i] + x[i-1] + … + x[i-7] >= r[i] (題目中的連續工作8小時)
   則有: 
         s[i] – s[i-1] >= 0, 1 <= i <= 24
         s[i-1] – s[i] >= –num[i], 1 <= i <= 24
         s[i] – s[i-8] >= r[i], 9 <= i <= 24
         s[i] – s[i+16] >= r[i] – s[24],  1<= i <= 8 
  (s[24]就是枚舉的ans,在枚舉的時候把它當成已知量ans,可以寫成s[i] – s[i+16] >= r[i] – ans)
   還需要添加一個隱藏不等式: s[24] – s[0] >= ans(枚舉的答案)
   這樣才能保證s[24]是大於等於ans的最小值。
   二分枚舉s[24],題目是求最小值,即建圖後以1爲源點求最長路,並判斷是否有負環。

AC代碼:

#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
using namespace std;
const int N=30,M=1e5+10,inf=0x7f7f7f7f;
bool vis[N];
int n,cnt,r[N],head[N],dis[N],tot[N],num[N];
struct edge
{
    int to,next,w;
}e[M<<1];
void add(int x,int y,int z)
{
    e[cnt].to=y;
    e[cnt].w=z;
    e[cnt].next=head[x];
    head[x]=cnt++;
}
bool judge(int mid)
{
    //連邊; 點下標爲0~24
    cnt=0;
    memset(head,-1,sizeof(head));
    for(int i=1;i<=24;i++)
    {
        if(i<=8)add(16+i,i,r[i]-mid);//i=8 16+i=24
        else add(i-8,i,r[i]);
        add(i,i-1,-num[i]);
        add(i-1,i,0);
    }
    add(0,24,mid);
    //SPFA 求最長路
    memset(dis,-inf,sizeof(dis));//注意初始化爲-inf(別寫成了inf)
    memset(tot,0,sizeof(tot));
    memset(vis,0,sizeof(vis));
    queue<int>q;
    q.push(0);
    vis[0]=1;
    dis[0]=0;
    while(!q.empty())
    {
        int u=q.front();q.pop();
        vis[u]=0;
        if(++tot[u]>24)return 0;//判環
        for(int i=head[u];i!=-1;i=e[i].next)
        {
            int v=e[i].to;
            if(dis[v]<dis[u]+e[i].w)//最長路
            {
                dis[v]=dis[u]+e[i].w;
                if(!vis[v])
                {
                    q.push(v);
                    vis[v]=1;
                }
            }
        }
    }
    return 1;
}
int main()
{
    ios::sync_with_stdio(false);
    int T,n,x;
    cin>>T;
    while(T--)
    {
        for(int i=1;i<=24;i++)//下標進行加1處理,從1開始
            cin>>r[i];
        cin>>n;
        memset(num,0,sizeof(num));
        for(int i=1;i<=n;i++)
        {
            cin>>x;
            num[x+1]++;//輸入的時刻+1
        }
        int L=0,R=n;
        int ans=-1;
        while(L<=R)//二分答案
        {
            int mid=(L+R)/2;
            if(judge(mid))ans=mid,R=mid-1;
            else L=mid+1;
        }
        if(ans==-1)printf("No Solution\n");
        else printf("%d\n",ans);
    }
    return 0;
}

洛谷 P3275 [SCOI2011]糖果

思路:

   x=1,A=B等價於A-B>=0且B-A>=0
   x=2,A<B等價於B-A>0,即B-A>=1
   x=3,A>=B等價於A-B>=0
   x=4,A>B等價於A-B>0,即A-B>=1
   x=5,A<=B等價於B-A>=0
   (A,B表示每個人得到的糖果a[]的下標)
   最後,還要求每個小朋友都要分到糖果,
   所以a[i]-a[0]>=1(1<=i<=n),則從0連到i一條邊,權值爲1。
   建圖完成後,跑一遍最長路,如果沒有負環,
   則dis[i]就是a[i]滿足約束條件的一個解(而且儘量小)。最後求和dis[i]即可。

這題我寫了幾次,都是TLE或者RE,坑的地方在於:

  1. 要特判 x=2或x=4 的時候A==B則直接輸出-1,否則再跑SPFA會TLE
  2. 必須逆序遍歷(n到1)連邊,否則TLE (此處特別玄學,SPFA的複雜度和連邊順序有關,數據卡了SPFA)
  3. 把邊的數組開小了,只開了1e5,然後RE(要避免這種低級錯誤就是先寫連邊的代碼,算完邊數之後再寫數組大小)

AC代碼:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+10,M=3e5+10,inf=0x7f7f7f7f;
int n,m,cnt,head[N],tot[N];
ll dis[N];
bool vis[N];
struct edge
{
    int to,w,next;
}e[M];
void add(int x,int y,int z)
{
    e[cnt].to=y;
    e[cnt].w=z;
    e[cnt].next=head[x];
    head[x]=cnt++;
}
ll spfa()
{
    queue<int>q;
    q.push(0);
    memset(dis,0,sizeof(dis));
    dis[0]=0;
    vis[0]=1;
    while(!q.empty())
    {
        int u=q.front();q.pop();
        vis[u]=0;
        if(++tot[u]>n)return -1;//有負環
        for(int i=head[u];i!=-1;i=e[i].next)
        {
            int v=e[i].to;
            int w=e[i].w;
            if(dis[v]<dis[u]+w)//最長路
            {
                dis[v]=dis[u]+w;
                if(!vis[v])
                {
                    q.push(v);
                    vis[v]=1;
                }
            }
        }
    }
    ll ans=0;
    for(int i=1;i<=n;i++)//求和
        ans+=dis[i];
    return ans;
}
int main()
{
    ios::sync_with_stdio(false);
    cin>>n>>m;
    memset(head,-1,sizeof(head));
    int opt,a,b;
    for(int i=1;i<=m;i++)
    {
        cin>>opt>>a>>b;
        if(a==b&&(opt==2||opt==4)){printf("-1\n");return 0;}
        //一定要特判,否則之後SPFA超時
        if(opt==1)add(b,a,0),add(a,b,0);
        else if(opt==2)add(a,b,1);
        else if(opt==3)add(b,a,0);
        else if(opt==4)add(b,a,1);
        else if(opt==5)add(a,b,0);
    }
    for(int i=n;i>=1;i--)//必須按逆序連邊,否則SPFA超時
        add(0,i,1);
    printf("%lld\n",spfa());
    return 0;
}

洛谷 P4878 [USACO05DEC]Layout G

思路:

   x,y表示位置a[]的下標,z表示權值
   (1) x-y<=z
   (2) x-y>=z => y-x<=-z
   (3) 隱含條件,位置必須按下標的順序排列,則a[i]-a[i-1]>=0,即a[i-1]-a[i]<=0
   化成這種形式(左邊未知數,右邊已知數,"<="的符號連接),"<="的符號說明是跑最短路。
   從1開始跑最短路,dis[1]=0,如果有解,得到dis[n],答案爲dis[n]-dis[1]=dis[n];
   怎麼判斷是否有解(無負環)呢?直接從1開始跑最短路就能正確判斷嗎?看看下面的細節分析。

細節上要注意的地方:

  1. 判斷是否有解,不能只跑一次SPFA。因爲圖不一定連通,從1開始跑SPFA,不一定能遍歷到所有點,可能存在負環,但是從1開始遍歷不到那個負環。爲了解決這個問題,需要建一個超級源點,下標爲0,把它與所有點連邊,從0開始跑一遍SPFA判斷是否有負環。
  2. 經過SPFA(0)之後,若無負環,則再正常跑SPFA(1)。如果無法到達終點,說明dis[n]可以任意大,輸出-2;否則輸出dis[n]。
#include <bits/stdc++.h>
using namespace std;
const int N=1e3+10,M=1e5+10,inf=0x7f7f7f7f;
int n,m1,m2,cnt,tot[N],head[N],dis[N];
bool vis[N];
struct edge
{
    int to,w,next;
}e[M];
void add(int x,int y,int z)
{
    e[cnt].to=y;
    e[cnt].w=z;
    e[cnt].next=head[x];
    head[x]=cnt++;
}
queue<int>q;
int spfa(int s)
{
    while(!q.empty())q.pop();
    memset(dis,inf,sizeof(dis));
    memset(vis,0,sizeof(vis));
    memset(tot,0,sizeof(tot));
    q.push(s);
    dis[s]=0;
    vis[s]=1;
    while(!q.empty())
    {
        int u=q.front();q.pop();
        vis[u]=0;
        if(++tot[u]>n)return -1;
        for(int i=head[u];i!=-1;i=e[i].next)
        {
            int v=e[i].to;
            int w=e[i].w;
            if(dis[v]>dis[u]+w)//最短路
            {
                dis[v]=dis[u]+w;
                if(!vis[v])
                {
                    q.push(v);
                    vis[v]=1;
                }
            }
        }
    }
    if(dis[n]==inf)return -2;//遍歷不到終點,dis[n]可以任意大
    return dis[n];
}
int main()
{
    ios::sync_with_stdio(false);
    cin>>n>>m1>>m2;
    int x,y,z;
    memset(head,-1,sizeof(head));
    for(int i=1;i<=m1;i++)
    {
        cin>>x>>y>>z;
        add(x,y,z);
    }
    for(int i=1;i<=m2;i++)
    {
        cin>>x>>y>>z;
        add(y,x,-z);
    }
    for(int i=1;i<=n;i++)
    {
        if(i!=1)add(i,i-1,0);
        add(0,i,0);
    }
    int ans=spfa(0);//先從0(超級源點)開始跑一次SPFA判斷是否有負環
    if(ans==-1)printf("-1\n");//有負環
    else printf("%d\n",spfa(1));//沒有負環的情況,從1跑一次求最短路dis[n]
    return 0;
}
/*
10 2 1
2 3 2
3 4 2
2 4 1012
ans:-1
*/
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章