差分约束 学习笔记

运动会不去宅在家里就是舒服,不接受某些人的批判(

记得两个半月前我就在学了,然后因为某些不能算是原因的原因就鸽掉了?我真是个🕊。

@Alex_Wei 通过多刷题 AK 了 IOI;@ET2006 AK 了 XJOI2030;我在学习普及组算法:我们都有光明的前途。


负环

有向图上的权值和为负的环叫做负环。很显然,有负环的图中至少有一组点的最短路不存在,因为可以绕着负环一直转一直转,没有最小只有更小。

负环的处理

Dijkstra 不能处理有负环的图,因为它连有负权边的图都处理不了()。因为 Dijkstra 每次找出的是当前没处理过的点中最短路最小的,用它来更新别人。而如果有负权边的话,那它就不能保证当前选出的最短路最小的点在以后都是最短路最小的了。

然后 Bellman-Ford 和 SPFA(队列优化版 Bellman-Ford)和 Floyd 都能处理的。如果没有负环就算出来就是最短路了呀。那如何判断是否有负环呢?Bellman-Ford 和 Floyd 都是如果按照正常流程结束之后(Bellman-Ford 是 \(n-1\) 轮结束后,Floyd 是三重循环结束后)依然能够成功松弛,那就有负环,反之没有;SPFA 没有所谓的「结束」,因为它的结束标志是队列空了,那就只能用某个点最短路经过的点数 \(>n\) 来判,这样复杂度还是保证着的。

顺便插一嘴 SPFA 怎么写,因为不常写:就是把堆优化 Dijkstra 的堆换成队列就可以了,然后要维护一个是否在队列里 inq instead of Dijkstra 里的是否被处理过 vis,在队列里就不进队了。

为什么要讲负环?

因为差分约束需要用到(

别说我抄袭进阶指南(

差分约束

差分约束系统

是指关于 \(n\) 个变量 \(x_{1\sim n}\) 的若干个形如 \(x_i-x_j\leq k\)\(k\) 是常数)的不等式组成的不等式组。

然后这个太局限性了。其他各种形式大多都可以转化来,特殊的有

  • \(x_i-x_j<k\Leftrightarrow x_i-x_j\leq k-\epsilon\)(在整域上时 \(\epsilon=1\));
  • \(x_i-x_j\geq k\Leftrightarrow x_j-x_i\leq -k\)
  • \(x_i-x_j=k\Leftrightarrow x_i-x_j\leq k,x_i-x_j\geq k\)
  • \(>\) 就随便转化吧();
  • \(x_i\leq k\),可以设一个 \(x_0=0\) 然后 \(x_i-x_0\leq k\)
  • ……()。

比较头疼的就是 \(x_i+x_j\leq k\) 还真就无法转化。

建立图论模型

注意到 \(x_i-x_j\leq k\) 可以转化为 \(x_i\leq x_j+k\),这与最短路中的三角形不等式 \(dis_i\leq dis_j+w(j,i)\) 相似。于是我们可以尝试从 \(j\)\(i\) 连一条长为 \(k\) 的边。

考虑对于任意一对互异的节点 \((i,j)\) 间的任意一条路径 \(p_{1\sim m}\),满足 \(x_{p_2}-x_{p_1}\leq w(p_1,p_2),x_{p_3}-x_{p_2}\leq w(p_2,p_3),\cdots,x_{p_n}-x_{p_{n-1}}\leq w(p_{n-1},p_n)\)。将这些不等式加起来得到 \(x_{p_n}-x_{p_1}\leq \sum\limits_{k=1}^{n-1}w(p_i,p_{i+1})\),即 \(x_j-x_i\leq \sum\limits_{k=1}^{n-1}w(p_i,p_{i+1})\)。也就是说路径和边的意义是一样的,两点之差不得超过它们之间任意一条路径长度。那么将它们间所有路径综合起来考虑,它一定要 \(\leq\) 最短的那条,也就是最短路。于是我们有 \(x_i-x_j\leq dis_{j,i}\)

解的存在性

显然,只要整张图中有负环,那就无解。因为一定存在一对点,使得它们之间的差的上限为无穷小,那就意味着不可能了。然后就判一下负环就可以了,照应前文(

有解情况下的常问问题

  • 求出任意一组解:注意到若 \(x_i=a_i\) 是一组解,那么 \(x_i=a_i+\Delta\) 一定也是一组解。不妨先求出一组全非正的解,然后再根据具体的题目条件加 \(\Delta\) 上去。考虑设一个 \(x_{-1}=0\)(为了和之前的 \(x_0\) 区分开,因为它们确实不能设成同一个东西),将 \(-1\) 和所有点连一条 \(0\) 边,跑 SPFA。这样一举两得,既可以一下将图中所有负环全部考虑一下(如果不这样,如果跑 SPFA 的话需要从每个点都出发一遍看是否有负环,这样就只需要跑一遍了),又可以求解。然后取 \(x_i=dis_i\) 显然是一组解,因为满足每一条三角形不等式。再提一下 \(x_0,x_{-1}\) 的事情,首先设成同一个东西显然可能把有解判成无解,然后如果分开设,就把 \(x_0\) 真的当作变量求出一组全非正解后,令 \(\Delta=-x_0\) 加回去可以得到一组满足 \(x_0=0\) 的解;
  • \(x_i-x_j\) 的最大 / 最小值:根据上面的理论,\(dis_{j,i}\)\(x_i-x_j\) 的上限,且可以达到。如果从 \(j\)\(i\) 没有路径,那就 \(dis_{j,i}=\infty\) 啊,自己理解。至于要求最小值,那就求 \(x_j-x_i\) 的最大值,然后取相反数。

例题

大概洛谷上所有标签为「差分约束」的紫题都在里面了,除了 P3084,那根本就不是差分约束,而是 DP(

P4878 - Layout G

这是一个差分约束的裸题吧……\(M_L\) 个限制显然可以表示为 \(x_B-x_A\leq D\),然后 \(M_D\) 个限制可以表示为 \(x_A-x_B\leq -D\)

但是有个比较坑的地方,就是图有可能不连通,直接从 \(1\) 跑一遍不一定能找到负环,要额外从 \(0\) 连向所有点跑一遍判负环。不过对于我这样思维缜密的人来说,这算不上坑点(伦敦雾

#include<bits/stdc++.h>
using namespace std;
#define mp make_pair
#define X first
#define Y second
#define pb push_back
const int inf=0x3f3f3f3f;
const int N=1000;
int n,ml,md;
vector<pair<int,int> > nei[N+1];
int dis[N+1];
bool inq[N+1];
int cnt[N+1];
void spfa(int x){
	queue<int> q;
	memset(dis,0x3f,sizeof(dis));
	dis[x]=0;cnt[x]=1;
	q.push(x);inq[x]=true;
	while(q.size()){
		int y=q.front();
		q.pop();
		inq[y]=false;
		for(int i=0;i<nei[y].size();i++){
			int z=nei[y][i].X,len=nei[y][i].Y;
			if(dis[y]+len<dis[z]){
				dis[z]=dis[y]+len;
				if((cnt[z]=cnt[y]+1)>n+1)puts("-1"),exit(0);
				if(!inq[z])q.push(z);
			}
		}
	}
}
int main(){
	cin>>n>>ml>>md;
	while(ml--){
		int x,y,d;
		cin>>x>>y>>d;
		nei[x].pb(mp(y,d));
	}
	while(md--){
		int x,y,d;
		cin>>x>>y>>d;
		nei[y].pb(mp(x,-d));
	}
	for(int i=1;i<=n;i++)nei[0].pb(mp(i,0));
	spfa(0);
	spfa(1);
	if(dis[n]==inf)puts("-2");
	else cout<<dis[n];
	return 0;
}

P4926 - 倍杀测量者

这题就很牛逼了……

首先答案 \(T\) 显然有单调性,先二分起来。(这是我第一次写实数倍增,就把 \(2\) 的次数弄到零下几十即可)

然后第一种 flag 是 \(\dfrac{x_A}{x_B}\leq k\) 就 nz 的形式,第二种 flag 是 \(\dfrac{x_B}{x_A}\geq k\) 就 nz 的形式。其中第二种等价于 \(\dfrac{x_A}{x_B}\leq \dfrac 1k\),所以两种都可以转化为 \(\dfrac{x_i}{x_j}\leq k\) 的形式。注意到这个长得很像差分约束,只不过把减法变成除法。恰好我们昨天刚学过对数可以变除为减,于是我们可以两边同时取对数(后来翻题解发现可以直接用乘法最短路,也就是广义的差分约束,不过不管了,本质相同),得到 \(\ln x_i-\ln x_j\leq \ln k\),也就是 \(x_i-x_j\leq k\) 的形式。

然后你需要判断的是是否对于所有取值,都有人 nz。考虑反面,是否存在取值,使得没人 nz。这个转化过其实就可以建出差分约束模型,然后判解的存在性了。这里命题的结论变否定了,所以上面的不等号还要取反(需要说明的是:由于这里是实数域上操作,所以严格与非严格不等号是一样的,不需要去管它)。

然后还有分数固定的人,这个就用 \(x_i=k\),转化为 \(x_i-x_0\leq k,x_i-x_0\geq k\) 然后搞就可以。

然后判解需要再设一个 \(x_{-1}\) 连所有点,这个上面说过了。

时间复杂度 \(\mathrm O(n(n+s)fuck)\),其中 \(fuck\) 是二分次数,是一个 \(\log\)

#include<bits/stdc++.h>
using namespace std;
#define mp make_pair
#define X first
#define Y second
#define pb push_back
const double inf=1e9;
const int N=1000,S=1000;
int n,s,t;
int o[S+1],a[S+1],b[S+1];double k[S+1];
int v[N+1];
vector<pair<int,double> > nei[N+2];
double dis[N+2];
bool inq[N+2];
int cnt[N+2];
bool spfa(int x=n+1){
	//remember to clear
	memset(inq,0,sizeof(inq)),memset(cnt,0,sizeof(cnt));
	queue<int> q;
	for(int i=0;i<=n+1;i++)dis[i]=inf;
	dis[x]=0;cnt[x]=1;
	q.push(x);inq[x]=true;
	while(q.size()){
		int y=q.front();
		q.pop();
		inq[y]=false;
		for(int i=0;i<nei[y].size();i++){
			int z=nei[y][i].X;double len=nei[y][i].Y;
			if(dis[y]+len<dis[z]){
				dis[z]=dis[y]+len;
				if((cnt[z]=cnt[y]+1)>n+2)return false;
				if(!inq[z])q.push(z);
			}
		}
	}
	return true;
}
bool chk(double x){
	//remember to clear
	for(int i=0;i<=n+1;i++)nei[i].clear();
	for(int i=1;i<=n;i++)if(v[i])nei[0].pb(mp(i,log(v[i]))),nei[i].pb(mp(0,-log(v[i])));
	for(int i=1;i<=s;i++){
		if(o[i]==1&&k[i]-x<=0||o[i]==2&&k[i]+x<=0)return false;
		int _o=o[i],_a=a[i],_b=b[i];double _k=_o==1?k[i]-x:k[i]+x;
		if(_o==2)_k=1/_k;
		_k=log(_k);
		swap(_a,_b),_k=-_k;
		nei[_b].pb(mp(_a,_k));
	}
	for(int i=0;i<=n;i++)nei[n+1].pb(mp(i,0));
	return !spfa();
}
int main(){
	cin>>n>>s>>t;
	for(int i=1;i<=s;i++)cin>>o[i]>>a[i]>>b[i]>>k[i];
	for(int i=1;i<=t;i++){
		int x,y;
		cin>>x>>y;
		v[x]=y;
	}
	double ans=0;
	for(int i=5;i>=-20;i--)if(chk(ans+pow(2,i)))ans+=pow(2,i);
	if(!chk(ans))puts("-1");
	else printf("%.10lf\n",ans);
	return 0;
}

P2474 - 天平

「差分约束的相对不是模板的题目」——chen_zhe

考虑将这个关系矩阵建出差分约束系统。然后还有隐含条件:\(1\leq x_i\leq 3\),也一并建进去。

然后我们考虑枚举另外两个砝码,然后看应该加到 \(c1,c2,c3\) 中哪一个里面去。

我们考虑枚举这四个砝码各自的重量,对于每一种看是否可能,如果可能就贡献到三种情况中的一种里面去。难点在于如何判断是否可能。

回想差分约束的理论:\(x_i-x_j\) 的上限是 \(dis_{j,i}\)。那么这四个节点如果两两符合,那就一定有一组解符合你想要的取值,否则没有。

最短路用 Floyd;判断的话平方枚举,然后暴力枚举值。所以总复杂度 \(\mathrm O\!\left(n^3+3^4n^2\right)\)

#include<bits/stdc++.h>
using namespace std;
const int N=50;
int n,a,b;
char mat[N+1][N+5];
int dis[N+1][N+1];
bool ok(int x,int vx,int y,int vy){return vx-vy<=dis[y][x]&&vy-vx<=dis[x][y];}
int main(){
	cin>>n>>a>>b;
	for(int i=1;i<=n;i++)cin>>mat[i]+1;
	memset(dis,0x3f,sizeof(dis));
	for(int i=0;i<=n;i++)dis[i][i]=0;
	for(int i=1;i<=n;i++)dis[i][0]=-1,dis[0][i]=3;
	for(int i=1;i<=n;i++)for(int j=1;j<=n;j++){
		if(mat[i][j]=='=')dis[i][j]=min(dis[i][j],0),dis[j][i]=min(dis[j][i],0);
		else if(mat[i][j]=='+')dis[i][j]=min(dis[i][j],-1);
		else if(mat[i][j]=='-')dis[j][i]=min(dis[j][i],-1);
	}
	for(int k=0;k<=n;k++)for(int i=0;i<=n;i++)for(int j=0;j<=n;j++)
		dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
	for(int k=0;k<=n;k++)for(int i=0;i<=n;i++)for(int j=0;j<=n;j++)
		if(dis[i][j]>dis[i][k]+dis[k][j])return puts("Fuck, this problem is shit!"),0;
//	for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)cout<<dis[i][j]<<" ";puts("");}
	int c1=0,c2=0,c3=0;
	for(int i=1;i<=n;i++)if(i!=a&&i!=b)for(int j=i+1;j<=n;j++)if(j!=a&&j!=b){
		bool flg1=false,flg2=false,flg3=false;
		for(int k=1;k<=3;k++)for(int o=1;o<=3;o++)for(int p=1;p<=3;p++)for(int q=1;q<=3;q++)
			if(ok(a,k,b,o)&&ok(a,k,i,p)&&ok(a,k,j,q)&&ok(b,o,i,p)&&ok(b,o,j,q)&&ok(i,p,j,q)){
				if(k+o>p+q)flg1=true;
				else if(k+o==p+q)flg2=true;
				else flg3=true;
			}
		if(flg1&&!flg2&&!flg3)c1++;
		if(!flg1&&flg2&&!flg3)c2++;
		if(!flg1&&!flg2&&flg3)c3++;
	}
	cout<<c1<<" "<<c2<<" "<<c3<<"\n";
	return 0;
}

P3530 - FES-Festival

题解

CF241E - Flights

呐,🕊 ycx 来补差分约束了?

我们考虑如果已知所有边的边权,那么如何判是否合法。考虑搞个 DP,\(dp_i\) 表示 \(1\to i\) 的所有路径的长度集合大小为 \(1\) 的情况下的最短路。注意 DP 过程中如果遇到不为 \(1\) 的直接输出不合法。然后这个地方有个坑点:题目只要求 \(1\to n\) 的路径长度唯一,并不要求其他的长度为一,也就是对于那些不在任何一条 \(1\to n\) 路径上的点,我们 dark 当他不存在。

那么我们能够通过一个合法的 DP 数组还原出每条边的权。于是我们只需要找出一组合法的 DP 数组。它显然要满足,对于所有边 \((x,y)\),都有 \(dp_y-dp_x\in\{1,2\}\)。那么可以列出不等式,然后差分约束。(注意这里似乎还要满足 \(dp_1=0\),不过无伤大雅,我们只研究 DP 值与 DP 值的差)

时间复杂度 \(\mathrm O(nm)\)

code

posted @ 2020-10-20 16:03  ycx060617  阅读(283)  评论(0编辑  收藏  举报