差分约束

负环与差分约束系统

负环

简单点说,就是我们的图上存在着一个环,使得环上总边权为负,这样的的环被称为负环,类似的,我们也有对正环的定义,需要注意的是,无向图中我们按两条相反有向边储存本身就等于是一个自环
对于存在负环的图,最短路问题永远不可能求出解,因为负环的存在会导致环上节点的三角不等式永远无法收敛,因为跑圈的同时会无限更新
类似的,对于存在正环的图,最长路问题也永远不可能求出解
介于正环和负环之间的就是零环,在一些题目中,零环没有任何意义,往往需要缩点缩掉
至于负环的求法,根据抽屉原理,一个负环必定会存在负权边,而存在负权边的最短路问题一般是采用Bellmanford或者是SPFA算法处理,类似的,我们可以使用它们来判断负环
边权为负的无向边本身就是一个负环,很多时候有向图数据经过构造有可能会出现重边,反向边等,需要注意,必要时特判

求法

  1. Bellmanford算法求负环:
    很简单,就是当经过了n1轮迭代之后再次扫描数组,若仍未收敛则证明存在负环
  2. SPFA求负环,对于SPFA求负环一般有两种方式
    第一种方式是:由于一个节点的入队次数代表着被更新的次数,按照SPFA的流程,若一个节点重复出队n次及以上,就存在着负环,具体的,我们可以用一个数组记录,出队时累加次数并判断即可
    第二种方式是:由于一个节点的更新次数与父节点的更新次数相关,于是我们可以使用cnt数组,初始全为零,当节点v被节点u更新时,则cnt[v]=cnt[u]+1,当cnt[v]n时,存在负环
    一般来说,第二种方式是优于第一种方式的,原因是第一种方式一般需要绕环n次,第二种方式只需要一次
    实现方式采用BFS
    一些常见优化
  3. 当数据范围过大的时候,普通SPFA算法复杂度难以承受,我们就可以设定一个阈值,当n,m的值较大,一般在5×104以上的时候(使用二分等增加复杂度的另算),当队列的出队次数大于这个阈值的时候就自动认为有负环,这个算法的正确性难以保证,但只要阈值计算合理,正确率极高,当然,一般需要自己构造数据或者人为找到这个阈值,根据经验,这个阈值一般不会小于五十倍的m,注意阈值的设定不可太高,否则会TLE,但也不可低,否则会WA
  4. 当图上大概率有负环的时候可以采用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 的一条路线为:T120D135S1G12K856

当某一条路线的末尾车次与另一条路线的开头车次相同时,这两条路线可以连接起来变为一条更长的行车路线。

显然若干条路线连接起来有可能构成一个环。

若有 3 条行车路线分别为:

x1x2x3
x3x4
x4x5x1
x1x5 车次的速度分别为 v1v5

定义高铁环的值为(环上各条行车路线速度和)的平均值,即:

[(v1+v2+v3)+(v3+v4)+(v4+v5+v1)]/3
所有高铁环的值的最大值称为最优高铁环的值。

给出 M 条行车路线,求最优高铁环的值(四舍五入为整数)。

分析

首先这个路线内部不需要管是什么,只需要知道两端即可,一条路线就等于是一条边,边权就是速度
不难发现,这道题涉及除法,并且最优高铁环的值明显可以二分,这就是一个0/1分数规划问题
我们设一个环为G=(V,E),V是点集,E是边集,那么我们的答案就是找到一个G,使得下式值最大

iEcost[i]uV1=iEcost[i]|V|

直接寻找明显很困难,下式又可以二分答案,那么我们考虑使用二分答案,设二分的值为mid,经过变式,有两种可能性
1.

iEcost[i]uV1mid

iEcost[i]uV1>mid

我们以2为例子进行分析
因为是有向图,所以环上的边一样可以使用以这条边为入边的节点来作为长度
变式即得

uV(midcost[u])<0

此时这个环就好找了,就是判定图中有没有负环,若有负环说明此式成立,令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);
}

差分约束系统

概述

所谓差分约束系统是指一个包含X1Xn的未知数,m个限制条件,每个限制条件是形如XiXjck的不等式,其中ck是任意常数,求其一组合法解
很明显,若我们找到了一组合法解,设为a1an,那么a+Δ,a2+Δan+Δ也是一组合法解,其中Δ为任意实数,因为两个变量做差会消去Δ
所以我们完全可以限制先找到一组负数解,然后通过变换找出所有解
我们发现,XiXjck进行变式之后XiXj+ck,这与三角形不等式很相似,这启发我们使用SPFA将其转换为图论问题进行求解,具体的,我们对于每一个约束条件都在图上加入边(j,i,ck),注意是j>i的有向边,最后如果这张图跑SPFA最短路最后能够收敛(无负环),就说明这个差分约束系统有解,其中一组解为dist数组,若无法收敛(存在负环),则差分约束系统无解
在实际应用中,差分约束系统常常不会将所有的限制条件摆在明面上,我们还需要在题目上挖掘隐藏条件使得解有意义,例如一个非严格单调递增序列就具备隐含条件,sksk1,这些往往可以从答案的相对大小关系,答案有意义的条件等方面进行寻找。有时,这个差分约束系统会变样,比如变为XiXjck,遇到这种情况一个解决办法是照样建图,找正环,最长路,另一个方案就是变式为XjXick进行处理,亦或者需要前缀和等东东,比如k=ii+dakck,其中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 个申请人可以从 ti 时刻开始连续工作 8 小时。

收银员之间不存在替换,一定会完整地工作 8 小时,收银台的数量一定足够。

现在给定你收银员的需求清单,请你计算最少需要雇佣多少名收银员。

分析

记每个时刻有s[i]个收银员可以开始工作,我们为了避免边界问题,将R数组整体向后平移一位,即平移后R[k]表示(k1):00k:00,特别的,R[0]表示24:000:00
f[i]表示在[0,i]的时间内(单位:hour),我们选用的开始工作和工作完成的收银员的总数量(体现了上文提到的前缀和思想),这个f一定满足以下条件

  1. f[0]=f[24],也即0f[0]f[24]0
  2. i[8,24],f[i]f[i8]R[i]
  3. i[0,7],f[i]+f[24]f[16+i]R[i]
  4. s[i]f[i]f[i1]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;
}
posted @   spdarkle  阅读(70)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示