差分约束

负环与差分约束系统

负环

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

求法

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

\[\frac{\sum_{i\in E}cost[i]}{\sum_{u\in V}1}=\frac{\sum_{i\in E}cost[i]}{|V|} \]

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

\[\frac{\sum_{i\in E}cost[i]}{\sum_{u\in V}1}\le mid \]

\[\frac{\sum_{i\in E}cost[i]}{\sum_{u\in V}1}>mid \]

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

\[\sum_{u\in V}(mid-cost[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);
}

差分约束系统

概述

所谓差分约束系统是指一个包含\(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\)一定满足以下条件

  1. \(f[0]=f[24]\),也即\(0\ge f[0]-f[24]\ge 0\)
  2. \(\forall i\in[8,24],f[i]-f[i-8]\ge R[i]\)
  3. \(\forall i\in[0,7],f[i]+f[24]-f[16+i]\ge R[i]\)
  4. \(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;
}
posted @ 2022-11-30 22:39  spdarkle  阅读(62)  评论(0编辑  收藏  举报