差分约束

简介

差分约束系统是一种特殊的多元一次不等式组。

nn 个点 XiX_imm 个约束条件组成。对于每个约束条件,都是由其中的两个变量做差得到。例如 XiXjCkijX_i-X_j\le C_k|i\neq jCkC_k 为常量。

我们需要将所有 XiX_i 解出来。差分约束问题的关键在于将所有的条件都建立成图中的边。


对于上式变号可得,XiCk+XjX_i\le C_k+X_j。假设已知 XjX_j 的值,就可以算出 XiX_i 的上界。若对于 XiX_i 有多个约束条件,则 XjX_j 的范围就是最小的 Xj+CkX_j+C_k

如果我们将每个 XiX_i 抽象成点,每个约束条件成为一条从 XjXiX_j\to X_i,边权为 CkC_k 的边,就是求最短路了。每次松弛就是更新 XiX_i,让 XiX_i 满足上式。

若对于该差分约束系统,有负环就意味着无解。因此,我们一般使用 SPFA\operatorname{SPFA} 算法求解。

为什么

理解为:每次松弛就是使 XiX_i 满足约束条件。无限的松弛说明不能满足条件。


同样的,假如变形后是 XiCk+XjX_i\geq C_k+X_j,我们就可以确定 XiX_i 的下界。对所有约束条件取最大。也即求最长路。

关于实现

有些时候条件不能将所有都 XX 串联在一起。即图是不连通的,此时利用超级原点思想,找一个没有值的点0,n+10,n+1 等),对所有点连接一条权为 00 的边。使得一次最短路可以求全图解。

对于 SPFA\operatorname{SPFA} 算法,一般的判断负环方法是:对于最短路,最多将图中每个节点遍历,也就是经过了 n1n-1 条边。当经过的边数 =n=n 时,就出现了负环。(超级原点设置后,会多出来一个点!


练习

小 k 的农场

差分约束入门题。以下提供最短路的做法。题意:

  • 如果每行的第一个数是 11,接下来有三个整数 a,b,ca,b,c,表示农场 aa 比农场 bb 至少多种植了 cc 个单位的作物;
  • 如果每行的第一个数是 22,接下来有三个整数 a,b,ca,b,c,表示农场 aa 比农场 bb 至多多种植了 cc 个单位的作物;
  • 如果每行的第一个数是 33,接下来有两个整数 a,ba,b,表示农场 aa 种植的的数量和 bb 一样多。

分别解决:

  • abca-b\geq c.变形为 acba-c\geq b
  • abca-b\leq c.变形为 bac    b+cab-a\geq -c\ \ \to\ \ b+c\ge a
  • a=ba=b.变形为 a+0b,b+0aa+0\geq b,b+0 \geq a

我们确定了解的上界,接下来要对所有的取最小值,也就是跑最短路。

Code

#include<bits/stdc++.h>
using namespace std;
const int N = 3e5+10;
int n,m;
int dis[N],num[N],vis[N];
int head[N],ne;
struct EDGE {int to,next,dis;}e[N<<1];
void ae(int from,int to,int dis) {e[++ne]={to,head[from],dis};head[from]=ne;}
string spfa(int root) {
	memset(dis,0x3f,sizeof dis);
	memset(num,0,sizeof num);
	memset(vis,0,sizeof vis);
	dis[root]=0;
	stack<int>q;
	q.push(root);
	while(!q.empty()) {
		int from=q.top();q.pop();
		vis[from]=0;
		for(int i=head[from];i;i=e[i].next) {
			int to=e[i].to,td=e[i].dis;
			if(dis[to]>dis[from]+td) {
				dis[to]=dis[from]+td;
				num[to]=num[from]+1;//记录经过的边数
				if(num[to]==n+1) return "No";//判断负环
				if(vis[to]==0) {q.push(to), vis[to]=1;}
			}
		}
	}
	int ans=0;
	for(int i=1;i<=n;i++) {
		ans+=dis[i];
	}
	return "Yes";
}
int main() {
	cin>>n>>m;
	for(int i=1,o,x,y,z;i<=m;i++) {
		cin>>o>>x>>y;
		if(o==1) cin>>z,ae(x,y,-z);
		if(o==2) cin>>z,ae(y,x,z);
		if(o==3) ae(y,x,0),ae(x,y,0);
	}
	for(int i=1;i<=n;i++) {//设立 0 为 超级原点
		ae(0,i,0); 
	}
	cout<<spfa(0);
	return 0;
}

P2294 [HNOI2005] 狡猾的商人

本题差分条件为在 lrl\sim r 区间内,收入为整数 ZZ
加入把每个月的收入使用前缀和。就会有 SrSl1=ZS_r-S_{l-1}=Z
对于相等的约束条件,就要使上,下界重合,即让 ,\geq,\leq 都取到等号。 SrSl1ZSrSl1+ZS_r-S_{l-1}\leq Z \to S_r\leq S_{l-1}+Z

SrSl1ZSl1SrZSl1Sr+ZS_r-S_{l-1}\geq Z \to S_{l-1}-S_r\leq Z \to S_{l-1}\leq S_r+Z

注意

l10l-1\ge0。超级原点不能在 0。可以选在 n+1n+1

Code

#include<bits/stdc++.h>
using namespace std;
const int N = 3e5+10;
int n,m;
int dis[N],num[N],vis[N];
int head[N],ne;
struct EDGE {int to,next,dis;}e[N<<1];
void ae(int from,int to,int dis) {e[++ne]={to,head[from],dis};head[from]=ne;}
string spfa(int root) {
	memset(dis,0x3f,sizeof dis);
	memset(num,0,sizeof num);
	memset(vis,0,sizeof vis);
	dis[root]=0;
	stack<int>q;
	q.push(root);
	while(!q.empty()) {
		int from=q.top();q.pop();
		vis[from]=0;
		for(int i=head[from];i;i=e[i].next) {
			int to=e[i].to,td=e[i].dis;
			if(dis[to]>dis[from]+td) {
				dis[to]=dis[from]+td;
				num[to]=num[from]+1;//记录经过的边数 
				if(num[to]==n+1) return "No";//判断负环 
				if(vis[to]==0) {q.push(to), vis[to]=1;}
			}
		}
	}
	int ans=0;
	for(int i=1;i<=n;i++) {
		ans+=dis[i];
	}
	return "Yes";
}
int main() {
	cin>>n>>m;
	for(int i=1,o,x,y,z;i<=m;i++) {
		cin>>o>>x>>y;
		if(o==1) cin>>z,ae(x,y,-z);
		if(o==2) cin>>z,ae(y,x,z);
		if(o==3) ae(y,x,0),ae(x,y,0);
	}
	for(int i=1;i<=n;i++) {//设立 0 为 超级原点 
		ae(0,i,0); 
	}
	cout<<spfa(0);
	return 0;
}

INTERVAL - Intervals

本题涉及到差分约束的一个关键:题目可能有隐含条件,要将所有条件都建图
除了最基本的条件,每个数最多被选一次,所以有 0SiSi110\le S_i-S_{i-1} \le 1


[1007]倍杀测量者

奇怪题目

本题中,以下讨论不需要女装的情况。

有两种不同的边:(A,B 表示选手的分数 )

  • A 选手 kTk-T 倍杀了 B。 A×(kT)BA\times(k-T)\geq B

  • A 选手没有被 B 选手 k+Tk+T 倍杀。A>B×(k+T)A > B\times(k+T)

要求最大的 TT 使方程无解,TT 的取值具有单调性。越小的 TT 就能满足条件,使小值最大,用二分答案解决。

但是乘法不能使用差分约束,考虑转化为加减法。使用幂的性质。

2a+b=2a×2b2^{a+b}=2^a\times 2^b

可以将上式中的 A×(kT)A\times (k-T) 转化为 2log2A+2log2kT2log2B2^{\log_2 A}+2^{\log_2 k-T}\ge 2^{log_2 B}。由于底数相同,只要比较指数大小,所以转变上式,得:

log2A+log2Blog2kT\log_2 A+\ge \log_2 B-\log_2 k-T log2A+log2B+log2k+T\log_2 A+\ge \log_2 B+\log_2 k+T

对于开始就确定的边权,看作第三种边,利用超级原点,得到 A=0+XiA=0+X_i。转变为:

A0+Xi&0XiAA\ge0+ X_i \And 0-X_i\leq A

二分 TT,如果有负环即合法。


实现

在实现中,由于有两种边有 log2A,log2B\log_2 A,\log_2 B 的存在,所以最短路中的 disdis 直接存储取对数后的值。为满足该条件,建第三种边时需要先 log\log

对于 k+T,kTk+T,k-TTT 的值有所改变。先存 kk,根据边的类型算出不同的值。

Code

#include<bits/stdc++.h>
using namespace std;
const int N = 3e5+10;
int n,m,m2;
double dis[N];int num[N],vis[N];
int head[N],ne;
struct EDGE {int to,next;double dis;int typ;}e[N<<1];
void ae(int from,int to,double dis,int typ) {e[++ne]={to,head[from],dis,typ};head[from]=ne;}
int spfa(int root,double t) {
	memset(dis,-0x3f,sizeof dis);
	memset(num,0,sizeof num);
	memset(vis,0,sizeof vis);
	dis[root]=0;
	stack<int>q;
	q.push(root);
	while(!q.empty()) {
		int from=q.top();q.pop();
		vis[from]=0;
		for(int i=head[from];i;i=e[i].next) {
			int to=e[i].to;double td=e[i].dis;
			if(e[i].typ==2) td=-log2(td+t);
			if(e[i].typ==1) td=log2(td-t);
			if(dis[to]<dis[from]+td) {
				dis[to]=dis[from]+td;
				num[to]=num[from]+1;
				if(num[to]>n+1) return 1; 
				if(vis[to]==0) {q.push(to), vis[to]=1;}
			}
		}
	}
	return 0;
}
double l,r=10,mid;
int main() {
	cin>>n>>m>>m2;
	for(int i=1,k,o,x,y;i<=m;i++) {
		cin>>o>>x>>y>>k;
		ae(y,x,k,o);
		if(o==1) r=min(r,k+0.00);//不能使 k-T < 0
	}
	for(int i=1,x,k;i<=m2;i++) {
		cin>>x>>k;
		ae(0,x,log2(k),3);
		ae(x,0,-log2(k),3);
	}
	while(l+1e-8<r) {
		mid=(l+r)/2;
		if(spfa(0,mid)) l=mid;
		else r=mid;
	}
	if(l==0) cout<<-1;
	else cout<<l;
 	return 0;
}
posted @ 2023-08-27 15:05  cjrqwq  阅读(4)  评论(0编辑  收藏  举报  来源