差分约束
负环与差分约束系统
负环
简单点说,就是我们的图上存在着一个环,使得环上总边权为负,这样的的环被称为负环,类似的,我们也有对正环的定义,需要注意的是,无向图中我们按两条相反有向边储存本身就等于是一个自环
对于存在负环的图,最短路问题永远不可能求出解,因为负环的存在会导致环上节点的三角不等式永远无法收敛,因为跑圈的同时会无限更新
类似的,对于存在正环的图,最长路问题也永远不可能求出解
介于正环和负环之间的就是零环,在一些题目中,零环没有任何意义,往往需要缩点缩掉
至于负环的求法,根据抽屉原理,一个负环必定会存在负权边,而存在负权边的最短路问题一般是采用\(Bellman-ford\)或者是\(SPFA\)算法处理,类似的,我们可以使用它们来判断负环
边权为负的无向边本身就是一个负环,很多时候有向图数据经过构造有可能会出现重边,反向边等,需要注意,必要时特判
求法
- \(Bellman-ford\)算法求负环:
很简单,就是当经过了\(n-1\)轮迭代之后再次扫描数组,若仍未收敛则证明存在负环 - \(SPFA\)求负环,对于\(SPFA\)求负环一般有两种方式
第一种方式是:由于一个节点的入队次数代表着被更新的次数,按照\(SPFA\)的流程,若一个节点重复出队\(n\)次及以上,就存在着负环,具体的,我们可以用一个数组记录,出队时累加次数并判断即可
第二种方式是:由于一个节点的更新次数与父节点的更新次数相关,于是我们可以使用\(cnt\)数组,初始全为零,当节点\(v\)被节点\(u\)更新时,则\(cnt[v]=cnt[u]+1\),当\(cnt[v]\ge n\)时,存在负环
一般来说,第二种方式是优于第一种方式的,原因是第一种方式一般需要绕环\(n\)次,第二种方式只需要一次
实现方式采用\(BFS\)
一些常见优化 - 当数据范围过大的时候,普通\(SPFA\)算法复杂度难以承受,我们就可以设定一个阈值,当\(n,m\)的值较大,一般在\(5\times 10^4\)以上的时候(使用二分等增加复杂度的另算),当队列的出队次数大于这个阈值的时候就自动认为有负环,这个算法的正确性难以保证,但只要阈值计算合理,正确率极高,当然,一般需要自己构造数据或者人为找到这个阈值,根据经验,这个阈值一般不会小于五十倍的\(m\),注意阈值的设定不可太高,否则会\(TLE\),但也不可低,否则会\(WA\)
- 当图上大概率有负环的时候可以采用\(DFS\)实现上面找负环的过程,需要注意的是,这样确实可以提高找到负环的效率,但若没有负环,复杂度极有可能达到上界\(O(nm)\),相反,\(BFS\)就很稳定,只是有负环并且环比较大的时候会跑满,所以除非图上极大概率有负环的情况下,一般不会使用这个方法
bool spfa(int mid){
memset(dis,0x3f,sizeof dis);
memset(cnt,0,sizeof cnt);
queue<int>q;
dis[0]=0;
q.push(0);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=head[u];i;i=nxt[i]){
int v=ver[i],w=cost[i];
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
cnt[v]=cnt[u]+1;
if(cnt[v]>=n)return 0;//存在负环
q.push(v);
}
}
}
return 1;//不存在负环
}
同理,正环的求法就是把最短路\(SPFA\)换成最长路\(SPFA\),照样更新即可
最优高铁环
幻影国建成了当今世界上最先进的高铁,该国高铁分为以下几类:
\(S\)—高速光子动力列车—时速 \(1000km/h\)
\(G\)—高速动车—时速 \(500km/h\)
\(D\)—动车组—时速 \(300km/h\)
\(T\)—特快—时速\(200km/h\)
\(K\)—快速—时速 \(150km/h\)
该国列车车次标号由上述字母开头,后面跟着一个正整数\((≤1000)\)构成。
由于该国地形起伏不平,各地铁路的适宜运行速度不同。
因此该国的每一条行车路线都由 \(K\) 列车次构成。
例如:\(K=5\) 的一条路线为:\(T120−D135−S1−G12−K856\)。
当某一条路线的末尾车次与另一条路线的开头车次相同时,这两条路线可以连接起来变为一条更长的行车路线。
显然若干条路线连接起来有可能构成一个环。
若有 3 条行车路线分别为:
\(x1−x2−x3\)
\(x3−x4\)
\(x4−x5−x1\)
\(x1∼x5\) 车次的速度分别为 \(v1∼v5\)。
定义高铁环的值为(环上各条行车路线速度和)的平均值,即:
\([(v1+v2+v3)+(v3+v4)+(v4+v5+v1)]/3\)
所有高铁环的值的最大值称为最优高铁环的值。
给出 \(M\) 条行车路线,求最优高铁环的值(四舍五入为整数)。
分析
首先这个路线内部不需要管是什么,只需要知道两端即可,一条路线就等于是一条边,边权就是速度
不难发现,这道题涉及除法,并且最优高铁环的值明显可以二分,这就是一个0/1分数规划问题
我们设一个环为\(G=(V,E)\),\(V\)是点集,\(E\)是边集,那么我们的答案就是找到一个\(G\),使得下式值最大
直接寻找明显很困难,下式又可以二分答案,那么我们考虑使用二分答案,设二分的值为\(mid\),经过变式,有两种可能性
1.
我们以2为例子进行分析
因为是有向图,所以环上的边一样可以使用以这条边为入边的节点来作为长度
变式即得
此时这个环就好找了,就是判定图中有没有负环,若有负环说明此式成立,令\(l=mid\),否则令\(r=mid\),二分结束时,就得到了答案
在本题中存在特殊构造的数据,具有重边和自环,对于每一个自环,答案一定不会小于这些自环的边权,在代码中见最后的一个\(\max\),至于重边,使用贪心不难证明重边只需要保留边权最小的一条即可
另外,本题数据非常紧,达到了\(50000\)的程度,需要使用上文所说的优化1进行优化,经实际测试50倍足以通过
#define N 50050
int num,ver[N],nxt[N],head[N],tot;float cost[N];float dis[N];int cnt[N],n,m,ms,to,mn=2e9;
struct node{
int u,v,w;
}que[N];
map<string,int>H;
map<pair<int,int>,pair<int,int> >edge;//判断重边,自环
int get(string n){
if(!H[n])H[n]=++num;
return H[n];
}//离散化,字符串化整数
int get_cost(char n){
if(n=='S')return 1000;
if(n=='G')return 500;
if(n=='D')return 300;
if(n=='T')return 200;
if(n=='K')return 150;
return 0;
}//得到权值
void add(int u,int v,int w){
nxt[++tot]=head[u],ver[tot]=v,cost[tot]=w,head[u]=tot;
}
void in(string x){
int u=-1,v=-1,w=0,len=x.size();
string y="";
for(int i=0;i<len;i++){
if(x[i]=='-'){
if(u==-1)u=get(y);
y="";
}
else w+=get_cost(x[i]),y+=x[i];
}
v=get(y);
ms+=w;
if(edge[make_pair(v,u)].second)mn=min(mn,edge[make_pair(v,u)].first);//自环
if(edge[make_pair(u,v)].second){//重边
if(edge[make_pair(u,v)].first<=w)return ;
edge[make_pair(u,v)].first=w;
que[edge[make_pair(u,v)].second].w=w;
return ;
}
que[++to]={u,v,w};
edge[make_pair(u,v)]=make_pair(w,to);
}
void init(float mid){//建新图
memset(head,0,sizeof head);
tot=1;
for(int i=1;i<=to;i++){
add(que[i].u,que[i].v,mid-que[i].w);
}
}
bool spfa(){
memset(cnt,0,sizeof cnt);
for(int i=1;i<=num;i++)dis[i]=2e9;
queue<int>q;
q.push(1);
dis[1]=0;
int t=0;
while(!q.empty()){
t++;
if(m>40000&&t>400000)return false;//优化1
int u=q.front();q.pop();
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(dis[v]>dis[u]+cost[i]){
cnt[v]=cnt[u]+1;
if(cnt[v]>=num)return false;
q.push(v);
dis[v]=dis[u]+cost[i];
}
}
}
return true;
}
float solve(){
float l=0,r=ms;
while(r-l>1e-1){
float mid=(l+r)/2;
init(mid);
if(spfa())r=mid;
else l=mid;
}
return l<1e-1?-1.5:l;//四舍五入
}
int main(){
cin>>m;
for(int i=1;i<=m;i++){
string x;
cin>>x;
in(x);
}
//puts("AS");
int ans=solve()+0.5;
if(mn<2e9)ans=max(ans,mn);
printf("%d",ans);
}
差分约束系统
概述
所谓差分约束系统是指一个包含\(X_1\sim X_n\)的未知数,\(m\)个限制条件,每个限制条件是形如\(X_i-X_j\le c_k\)的不等式,其中\(c_k\)是任意常数,求其一组合法解
很明显,若我们找到了一组合法解,设为\(a_1\sim a_n\),那么\(a+\Delta,a_2+\Delta……a_n+\Delta\)也是一组合法解,其中\(\Delta\)为任意实数,因为两个变量做差会消去\(\Delta\)
所以我们完全可以限制先找到一组负数解,然后通过变换找出所有解
我们发现,\(X_i-X_j\le c_k\)进行变式之后\(X_i\le X_j+c_k\),这与三角形不等式很相似,这启发我们使用\(SPFA\)将其转换为图论问题进行求解,具体的,我们对于每一个约束条件都在图上加入边\((j,i,c_k)\),注意是\(j->i\)的有向边,最后如果这张图跑\(SPFA\)最短路最后能够收敛(无负环),就说明这个差分约束系统有解,其中一组解为\(dist\)数组,若无法收敛(存在负环),则差分约束系统无解
在实际应用中,差分约束系统常常不会将所有的限制条件摆在明面上,我们还需要在题目上挖掘隐藏条件使得解有意义,例如一个非严格单调递增序列就具备隐含条件,\(s_k\ge s_{k-1}\),这些往往可以从答案的相对大小关系,答案有意义的条件等方面进行寻找。有时,这个差分约束系统会变样,比如变为\(X_i-X_j\ge c_k\),遇到这种情况一个解决办法是照样建图,找正环,最长路,另一个方案就是变式为\(X_j-X_i\le -c_k\)进行处理,亦或者需要前缀和等东东,比如\(\sum_{k=i}^{i+d}a_k\le c_k\),其中\(d,i\)是常数,这种就可以使用前缀和来变成一般形式进行处理,有时我们会涉及到某些约束条件变成了三元,但多出来的一元都是一样的,这样我们可以二分答案进行处理,具体例子由下面的例题给出
雇佣收银员
一家超市要每天 \(24\) 小时营业,为了满足营业需求,需要雇佣一大批收银员。
已知不同时间段需要的收银员数量不同,为了能够雇佣尽可能少的人员,从而减少成本,这家超市的经理请你来帮忙出谋划策。
经理为你提供了一个各个时间段收银员最小需求数量的清单 \(R(0),R(1),R(2),…,R(23)\)。
\(R(0)\) 表示午夜 \(00:00\) 到凌晨 \(01:00\) 的最小需求数量,\(R(1)\) 表示凌晨 \(01:00\) 到凌晨 \(02:00\) 的最小需求数量,以此类推。
一共有\(N\) 个合格的申请人申请岗位,第 \(i\) 个申请人可以从 \(t_i\) 时刻开始连续工作 \(8\) 小时。
收银员之间不存在替换,一定会完整地工作 8 小时,收银台的数量一定足够。
现在给定你收银员的需求清单,请你计算最少需要雇佣多少名收银员。
分析
记每个时刻有\(s[i]\)个收银员可以开始工作,我们为了避免边界问题,将\(R\)数组整体向后平移一位,即平移后\(R[k]\)表示\((k-1):00\sim k:00\),特别的,\(R[0]\)表示\(24:00\sim 0:00\)
设\(f[i]\)表示在\([0,i]\)的时间内(单位:hour),我们选用的开始工作和工作完成的收银员的总数量(体现了上文提到的前缀和思想),这个\(f\)一定满足以下条件
- \(f[0]=f[24]\),也即\(0\ge f[0]-f[24]\ge 0\)
- \(\forall i\in[8,24],f[i]-f[i-8]\ge R[i]\)
- \(\forall i\in[0,7],f[i]+f[24]-f[16+i]\ge R[i]\)
- \(s[i]\ge f[i]-f[i-1]\ge 0\)
对于\(f\)数组,我们发现可以使用差分约束系统进行求解,在代码实现中我使用的是最长路找正环的解法(上文提到的此种情况的解决方案1)
不过需要注意的是,对于第三个条件突兀的冒出来了个\(f[24]\),并且\(f[24]\)也是最终的答案,明显其是具有单调性的,我们可以二分求这个值,使用差分约束系统判断是否有解,毕竟题目也是要让求最小值
int n,m,t,k,head[25],cost[105],ver[105],nxt[105],tot,R[25],s[25],dis[25],cnt[105];
void add(int u,int v,int w){
nxt[++tot]=head[u],ver[tot]=v,cost[tot]=w,head[u]=tot;
}
bool spfa(int mid){
memset(dis,0xcf,sizeof dis);
memset(cnt,0,sizeof cnt);
queue<int>q;
dis[0]=0;
q.push(0);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=head[u];i;i=nxt[i]){
int v=ver[i],w=cost[i]-(i>65)*mid;
if(dis[v]<dis[u]+w){
dis[v]=dis[u]+w;
cnt[v]=cnt[u]+1;
if(cnt[v]>=25)return 0;
q.push(v);
}
}
}
return 1;
}
int main() {
scanf("%d",&t);
while(t--){
tot=0;
for(int i=1;i<=24;i++)scanf("%d",&R[i]);
scanf("%d",&n);
memset(head,0,sizeof head);
memset(s,0,sizeof s);
for(int i=1;i<=n;i++){
scanf("%d",&m);
++s[m+1];
}
for(int i=1;i<=24;i++)add(i,i-1,-s[i]),add(i-1,i,0);
for(int i=8;i<=24;i++)add(i-8,i,R[i]);
for(int i=1;i<=7;i++)add(i+16,i,R[i]);
add(24,0,0);
add(0,24,0);
int l=0,r=n+1;
while(l<=r) {
int mid=l+r>>1;
cost[tot]=mid<<1;
if(spfa(mid))r=mid-1;
else l=mid+1;
}
if(r==n+1)printf("No Solution\n");
else printf("%d\n",l);
}
return 0;
}