差分约束
负环与差分约束系统
负环
简单点说,就是我们的图上存在着一个环,使得环上总边权为负,这样的的环被称为负环,类似的,我们也有对正环的定义,需要注意的是,无向图中我们按两条相反有向边储存本身就等于是一个自环
对于存在负环的图,最短路问题永远不可能求出解,因为负环的存在会导致环上节点的三角不等式永远无法收敛,因为跑圈的同时会无限更新
类似的,对于存在正环的图,最长路问题也永远不可能求出解
介于正环和负环之间的就是零环,在一些题目中,零环没有任何意义,往往需要缩点缩掉
至于负环的求法,根据抽屉原理,一个负环必定会存在负权边,而存在负权边的最短路问题一般是采用
边权为负的无向边本身就是一个负环,很多时候有向图数据经过构造有可能会出现重边,反向边等,需要注意,必要时特判
求法
算法求负环:
很简单,就是当经过了 轮迭代之后再次扫描数组,若仍未收敛则证明存在负环 求负环,对于 求负环一般有两种方式
第一种方式是:由于一个节点的入队次数代表着被更新的次数,按照 的流程,若一个节点重复出队 次及以上,就存在着负环,具体的,我们可以用一个数组记录,出队时累加次数并判断即可
第二种方式是:由于一个节点的更新次数与父节点的更新次数相关,于是我们可以使用 数组,初始全为零,当节点 被节点 更新时,则 ,当 时,存在负环
一般来说,第二种方式是优于第一种方式的,原因是第一种方式一般需要绕环 次,第二种方式只需要一次
实现方式采用
一些常见优化- 当数据范围过大的时候,普通
算法复杂度难以承受,我们就可以设定一个阈值,当 的值较大,一般在 以上的时候(使用二分等增加复杂度的另算),当队列的出队次数大于这个阈值的时候就自动认为有负环,这个算法的正确性难以保证,但只要阈值计算合理,正确率极高,当然,一般需要自己构造数据或者人为找到这个阈值,根据经验,这个阈值一般不会小于五十倍的 ,注意阈值的设定不可太高,否则会 ,但也不可低,否则会 - 当图上大概率有负环的时候可以采用
实现上面找负环的过程,需要注意的是,这样确实可以提高找到负环的效率,但若没有负环,复杂度极有可能达到上界 ,相反, 就很稳定,只是有负环并且环比较大的时候会跑满,所以除非图上极大概率有负环的情况下,一般不会使用这个方法
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;//不存在负环
}
同理,正环的求法就是把最短路
最优高铁环
幻影国建成了当今世界上最先进的高铁,该国高铁分为以下几类:
该国列车车次标号由上述字母开头,后面跟着一个正整数
由于该国地形起伏不平,各地铁路的适宜运行速度不同。
因此该国的每一条行车路线都由
例如:
当某一条路线的末尾车次与另一条路线的开头车次相同时,这两条路线可以连接起来变为一条更长的行车路线。
显然若干条路线连接起来有可能构成一个环。
若有 3 条行车路线分别为:
定义高铁环的值为(环上各条行车路线速度和)的平均值,即:
所有高铁环的值的最大值称为最优高铁环的值。
给出
分析
首先这个路线内部不需要管是什么,只需要知道两端即可,一条路线就等于是一条边,边权就是速度
不难发现,这道题涉及除法,并且最优高铁环的值明显可以二分,这就是一个0/1分数规划问题
我们设一个环为
直接寻找明显很困难,下式又可以二分答案,那么我们考虑使用二分答案,设二分的值为
1.
我们以2为例子进行分析
因为是有向图,所以环上的边一样可以使用以这条边为入边的节点来作为长度
变式即得
此时这个环就好找了,就是判定图中有没有负环,若有负环说明此式成立,令
在本题中存在特殊构造的数据,具有重边和自环,对于每一个自环,答案一定不会小于这些自环的边权,在代码中见最后的一个
另外,本题数据非常紧,达到了
#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);
}
差分约束系统
概述
所谓差分约束系统是指一个包含
很明显,若我们找到了一组合法解,设为
所以我们完全可以限制先找到一组负数解,然后通过变换找出所有解
我们发现,
在实际应用中,差分约束系统常常不会将所有的限制条件摆在明面上,我们还需要在题目上挖掘隐藏条件使得解有意义,例如一个非严格单调递增序列就具备隐含条件,
雇佣收银员
一家超市要每天
已知不同时间段需要的收银员数量不同,为了能够雇佣尽可能少的人员,从而减少成本,这家超市的经理请你来帮忙出谋划策。
经理为你提供了一个各个时间段收银员最小需求数量的清单
一共有
收银员之间不存在替换,一定会完整地工作 8 小时,收银台的数量一定足够。
现在给定你收银员的需求清单,请你计算最少需要雇佣多少名收银员。
分析
记每个时刻有
设
,也即
对于 数组,我们发现可以使用差分约束系统进行求解,在代码实现中我使用的是最长路找正环的解法(上文提到的此种情况的解决方案1)
不过需要注意的是,对于第三个条件突兀的冒出来了个 ,并且 也是最终的答案,明显其是具有单调性的,我们可以二分求这个值,使用差分约束系统判断是否有解,毕竟题目也是要让求最小值
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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!