本文主要是記錄差分約束系統練習題的思路和代碼,在此不再詳細解釋算法原理。
差分約束系統原理,參考文章: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,坑的地方在於:
- 要特判 x=2或x=4 的時候A==B則直接輸出-1,否則再跑SPFA會TLE
- 必須逆序遍歷(n到1)連邊,否則TLE (此處特別玄學,SPFA的複雜度和連邊順序有關,數據卡了SPFA)
- 把邊的數組開小了,只開了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開始跑最短路就能正確判斷嗎?看看下面的細節分析。
細節上要注意的地方:
- 判斷是否有解,不能只跑一次SPFA。因爲圖不一定連通,從1開始跑SPFA,不一定能遍歷到所有點,可能存在負環,但是從1開始遍歷不到那個負環。爲了解決這個問題,需要建一個超級源點,下標爲0,把它與所有點連邊,從0開始跑一遍SPFA判斷是否有負環。
- 經過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
*/