1.思路
没什么好讲的,刘汝佳的书《算法艺术与信息学竞赛》讲得很好,不过对于初次接触差分约束系统的人来讲还是很费解,可以去参考《算法导论》,里面有很详细的约束图的构建过程。这里讲一下我的个人理解。s[i]为从0至i时刻雇佣的人数,t[i]为i时刻应聘的人数,r[i]为i至(i+1)%24时间段需要的最少人数,得出约束条件:
s[i]-s[i-1]>=0
s[i]-s[i-1]<=t[i] 〈==〉s[i-1]-s[i]>=-t[i]
s[i]-s[-1]>=sum
s[i]-s[j]>=r[i] i>j,i=(j+8)mod24
s[i]-s[j]>=r[i]-sum i<j,i=(j+8)mod24
i的范围应该是1~23,因为-1时刻是没有定义的,但是这样的话0时刻的约束s[0]>=0,s[0]<=t[0]就没有考虑在内,为了引入这两个约束条件我们设s[-1]=0,则s[0]=s[0]-s[-1]>=0,
s[0]-s[-1]<=t[0],因此可以把i的范围调整为0~23,这样可以避免对0时刻的特殊讨论!我们可以把-1作为一个参考点s,,23作为目标点t,我取-1为24,连接参考点至每一个点一条边,边权为0(因为每个点都有可能约束其它点),相当于给每个点赋了一个基础值d=0,个人觉得随便赋值多少都没关系。导论上也说了,若<x0,x1……xn-1>为约束系统的解,则<x0+d,x1+d,……xn-1 +d>也是系统的解,只不过我们这里是要求一个最少的人数因此取为d=0。最后从小到大枚举sum,求约束系统里单源s至其它点的最长路(因为这里取的都是大于关系,举一个最简单的例子a>=b同时对c约束,满足c>=a,c>=b,显然这时我们要取c>=a才能使c>=a,c>=b这两个条件都满足!相反的若取小于等于关系则是求单源最短路!导论上就是这样的。)若不存在正圈(通过走正圈约束永远无法得到全部满足)则有解退出,否则无解!
有个地方很费解:用SPFA()会错!
2.个人代码
#include<cstdio> #include<iostream> using namespace std; // freopen("data.in","r",stdin); #include<algorithm> #include<cstring> #define N 30 #define INF 1000000000//!! #define WT 8//!! struct Graph { int mat[N][N]; int dis[N]; int que[N],front,rear; bool vis[N]; int in[N]; int r[N],tt[N]; int _V,s,t; void init() { _V=24; s=24,t=23;//!!24做为最短路的源点 int u; for(u=0; u<=_V; u++) { for(int v=0; v<=_V; v++) { mat[u][v]=-INF; } } for(u=0; u<=_V; u++)mat[s][u]=0; memset(tt,0,sizeof(tt)); } void input() { int i; for(i=0; i<_V; i++)scanf("%d",r+i); int n; cin>>n; for(i=0; i<n; i++) { int T; scanf("%d",&T); tt[T]++; } } void creatGraph() { init(); input(); int u; for(u=1; u<_V; u++) { int v=u-1; mat[v][u]=0; mat[u][v]=-tt[u]; } mat[s][0]=0; mat[0][s]=-tt[0];//!!忘记加这个约束了,导致无限WA for(u=0; u<_V; u++) { int v=(u+WT)%_V; mat[u][v]=r[v]; } } bool spfa() { fill(dis,dis+_V,-INF); dis[s]=0; memset(vis,0,sizeof(vis)); memset(in,0,sizeof(in)); front=rear=0; que[rear++]=s; vis[s]=true; in[s]++; while(front!=rear) { int u=que[front++]; front%=N; vis[u]=false; for(int v=0; v<_V; v++) { if(mat[u][v]>-INF&&dis[v]<dis[u]+mat[u][v]) { dis[v]=dis[u]+mat[u][v]; if(!vis[v]) { que[rear++]=v; rear%=N; vis[v]=true; in[v]++; if(in[v]>_V)return false; } } } } return true; } void set() { for(int u=16; u<_V; u++)//!! { int v=(u+WT)%_V; mat[u][v]--; } } bool bellmanFord() { fill(dis,dis+_V,-INF); dis[s]=0; for(int k=1; k<_V; k++) { for(int u=0; u<=_V; u++) { for(int v=0; v<=_V; v++) { if(mat[u][v]>-INF&&dis[v]<dis[u]+mat[u][v]) { dis[v]=dis[u]+mat[u][v]; } } } } for(int u=0; u<=_V; u++) { for(int v=0; v<=_V; v++) { if(mat[u][v]>-INF&&dis[v]<dis[u]+mat[u][v])return false; } } return true; } bool SODC(int &cost) { int up=0; for(int i=0; i<_V; i++)up+=tt[i]; for(int sum=1; sum<=up; sum++) { mat[s][t]=sum; set(); if(bellmanFord()) { cost=sum; return true; } } return false; } } net; int main() { // freopen("data.in","r",stdin); int Case; cin>>Case; while(Case--) { net.creatGraph(); int cost; if(net.SODC(cost))cout<<cost; else cout<<"No Solution"; cout<<endl; } return 0; }
#include <iostream> using namespace std; const int M = 25; const int INF = 21000000; int r[M]; //每小时需要的出纳员的数目 int t[M]; //每小时应征的申请者的数目 int s[M]; //0~i时刻雇用的出纳员的数目 int main(void) { int ts; cin >> ts; while (ts--) { for (int i = 1; i <= 24; i++) { cin >> r[i]; t[i] = 0; } int m, a; cin >> m; for (int i = 0; i < m; i++) { cin >> a; t[a+1]++; } int sum; for (sum = 0; sum <= m; sum++) //枚举sum { s[0] = 0; for (int i = 1; i <= 24; i++) s[i] = INF; int count = 0; bool flag = 1; while (flag&&count < M) //进行m-1松弛操作 { flag = 0; count++; for (int i = 1; i <= 24; i++) if (s[i] < s[i-1]) { s[i-1] = s[i]; flag = 1; } for (int i = 24; i > 0; i--) if (s[i-1] + t[i] < s[i]) { s[i] = s[i-1]+t[i]; flag = 1; } if (s[24] - sum < s[0]) { s[0] = s[24] - sum; flag = 1; } for (int i = 1; i <= 24; i++) { if (i > 8 && s[i] < s[i-8] + r[i]) { s[i-8] = s[i] - r[i]; flag = 1; } if (i <= 8 && s[i] < s[i+16] + r[i] - sum) { s[i+16] = s[i] - r[i] + sum; flag = 1; } } } if (count < M && s[24] >= sum) break; } if (sum > m) cout << "No Solution" << endl; else cout << sum << endl; } return 0; }
3.二分枚举的话用下面这段代码(直接枚举141MS,黑书上的是47MS,二分枚举16MS)。
void set(int mid,int sign) { for(int u=16; u<_V; u++)//!! { int v=(u+WT)%_V; mat[u][v]+=mid*sign; } } bool SODC(int &cost) { int right=0,left=1,mid; for(int i=0; i<_V; i++)right+=tt[i]; while(true) { mid=(right+left)>>1; mat[s][t]=mid; set(mid,-1); if(bellmanFord()) { if(left==right) { cost=mid; return true; } else right=mid; } else { if(left==right)return false; else left=mid+1; } set(mid,1); } }
4.拓展及补充
一、若有重边该怎么取?
当然这个题目不会,可是如果改3小时的,如下面的案例:
1
3
1 0 0
1
1
从小到大枚举答案当ans=1时
G[2][1]有两个取值0和-1。这时该怎么取呢?或者这时差分约束已经不适应了?
我构的图源点s=3,目标点t=2
构出的图矩阵表示为
-inf 0 0 -inf
0 -inf 0 -inf
-inf 0/-1 -inf -inf
0 0 0 0
最后一行零表示源点到每一个的距离为0,-inf为不可达,自身与自身的不可达是为了spfa时不加判断语句v!=u。
二、还有我对从源点s至目标t的边的理解是dis[t]=ans>0在图中可能会构成正圈,也就是无解的状态,但是不加这条边的话,本来有正圈的(无解)会当成了无正圈(有解)输出!