(笔记)差分约束

为什么我在记别人两年前学过的东西??

思想

若干个变量构成了相互 \(m\) 条约束形如:

\[\begin{cases} x_{a_1}+w_1\geq x_{b_1}\\ x_{a_2}+w_2\geq x_{b_2}\\ \dots\\ x_{a_m}+w_m\geq x_{b_m} \end{cases} \]

显然可以转化为 \((a_i,b_i,w_i)\) 的三元组中,在图上 \(a_i\to b_i\) 连边,并且跑一遍最短路算法。当然如果存在负环会出问题,如果 \(O(nm)\) 的复杂度被允许,直接跑 SPFA 顺便判断负环即可。否则需要观察题目的特殊性然后通过 Tarjan 缩点等方式把最短路约束转换为 DAG 上 DP 等形式。

例题

P5960 【模板】差分约束

判负环有两种方法,具体见(笔记)Dijkastra Bellman-Ford SPFA Floyd 最短路算法,这里采用的是第二种。对于加法来说,这些 \(x_i\) 的关系都是相对的,因此可以随便给 \(x_i\) 整体加上或减去一个数,由于图不一定联通,我们不妨让所有 \(x_i\)\(\le 0\),即连一条 \(dis_{n+1}=0\)\(n+1\) 到所有点的约束边,然后就能求出一组特殊解。

#include<bits/stdc++.h>
using namespace std;
const int N=5e3+5,INF=1e9;
int n,m,idx,head[N];
struct Edge{int u,v,w;}e[N<<1];
int dis[N][2];
int main(){
  ios::sync_with_stdio(0);
  cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1;i<=m;i++){
    int u,v,w;cin>>u>>v>>w;
    e[i]=(Edge){v,u,w};
  }
  for(int i=1;i<=n+1;i++)
    dis[i][0]=dis[i][1]=INF;
  for(int i=1;i<=n;i++)
    e[++m]=(Edge){n+1,i,0};
  dis[n+1][0]=0;
  for(int K=1;K<=n;K++){
    for(int i=1;i<=n+1;i++)
      dis[i][K&1]=dis[i][!(K&1)];
    for(int i=1;i<=m;i++){
      int u=e[i].u,v=e[i].v,w=e[i].w;
      dis[v][K&1]=min(dis[v][K&1],dis[u][!(K&1)]+w);
    }
  }
  bool tf=1;
  for(int i=1;i<=m;i++)
    if(dis[e[i].u][n&1]+e[i].w<dis[e[i].v][n&1]){tf=0;break;}
  if(!tf)cout<<"NO";
  else for(int i=1;i<=n;i++)cout<<dis[i][n&1]<<' ';
  return 0;
}

P5590 赛车游戏

把变量 \(x_i\) 定为图中节点到 \(1\) 的最短路长度,约束是有向边连边使得 \(u\to v\)\(1\le dis_v-dis_u\le 9\),放进差分约束模型里跑一边就行了,实际边权就是跑出来的 \(dis_v-dis_u\)。当然这里的仅仅规定了 \(1\to n\) 所有路径长度相等,所以约束只需要看那些在该路径上的边即可。怎么找出路径上的点?建正反图分别以 \(1,n\) 开始跑 DFS,终点分别为 \(n,1\),记录经过的节点并打标记,如果有节点有两个标记说明在该路径上。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e3+5,INF=1e9;
int n,m,U[N*2],V[N*2];
bool vis[N],vis1[N],flg[N];
int cnte;
vector<int>G[N],nG[N];
struct Edge{int u,v,w;}e[N<<2];
int dis[2][N];
void dfs(int u){
	vis[u]=1;
	if(u==n)return ;
	for(int v:G[u])
		if(!vis[v])dfs(v);
}
void dfs1(int u){
	vis1[u]=1;
	if(u==1)return ;
	for(int v:nG[u])
		if(!vis1[v])dfs1(v);
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v,w;cin>>u>>v;
		U[i]=u,V[i]=v;
		G[u].push_back(v);
		nG[v].push_back(u);
	}
	dfs(1);dfs1(n);
	if(!vis[n]){
		cout<<'-'<<'1'<<'\n';
		return 0;
	}
	for(int i=1;i<=n;i++)
		flg[i]=vis[i]&vis1[i];
	for(int i=1;i<=m;i++){
		int u=U[i],v=V[i];
		if(flg[u]&&flg[v]){
			e[++cnte]=(Edge){u,v,9};
			e[++cnte]=(Edge){v,u,-1};
		}
	}
	for(int i=1;i<=n;i++)
		dis[0][i]=INF;
	dis[0][1]=0;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++)
			dis[i&1][j]=dis[!(i&1)][j];
		for(int j=1;j<=cnte;j++){
			int u=e[j].u,v=e[j].v;
			int w=e[j].w;
			if(dis[i&1][v]>dis[!(i&1)][u]+w)
				dis[i&1][v]=dis[!(i&1)][u]+w;
		}
	}
	for(int j=1;j<=cnte;j++){
		int u=e[j].u,v=e[j].v;
		int w=e[j].w;
		if(dis[n&1][v]>dis[n&1][u]+w){
			cout<<'-'<<'1'<<'\n';
			return 0;
		}
	}
	cout<<n<<' '<<m<<'\n';
	for(int i=1;i<=m;i++){
		cout<<U[i]<<' '<<V[i]<<' ';
		if(flg[U[i]]&&flg[V[i]])cout<<dis[n&1][V[i]]-dis[n&1][U[i]]<<'\n';
		else cout<<'1'<<'\n';
	}
	return 0;
}

P4926 [1007] 倍杀测量者

二分答案 + 差分约束的思路比较显然,唯一问题是如何处理 \(x_{a_i}w_i\geq x_{b_i}\) 的问题,直接无脑套乘法转移最短路。也可以把所有数转化为 \(\log_2 a\) 的形式,乘法就是直接相加。我不管反正我无脑套乘法转移最短路

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const long double eps=1e-6;
const int N=1e3+5;
int n,m,t,A[N],B[N],OP[N],K[N];
int id[N],ori[N];
long double dis[2][N];
int ecnt;
bool giv[N];
struct edge{int u,v;long double w;}e[N<<3];
bool check(long double T){
	ecnt=0;
	for(int i=1;i<=n;i++){
		int op=OP[i],a=A[i];
		int b=B[i],k=K[i];
		if(op==1)e[++ecnt]=(edge){a,b,1.0/(k-T)};
		else e[++ecnt]=(edge){a,b,k+T};
	}
	for(int i=1;i<=n;i++)
		e[++ecnt]=(edge){i,0,1};
	dis[0][0]=0;giv[0]=1;
	for(int i=1;i<=n;i++)
		dis[0][i]=1e14;
	for(int i=1;i<=t;i++)
		dis[0][id[i]]=ori[i];
	for(int i=1;i<=n+1;i++){
		for(int j=0;j<=n;j++)
			dis[i&1][j]=dis[!(i&1)][j];
		for(int j=1;j<=ecnt;j++){
			int u=e[j].u,v=e[j].v;
			long double w=e[j].w;
			if(!giv[v]&&dis[i&1][v]>dis[!(i&1)][u]*w)
				dis[i&1][v]=dis[!(i&1)][u]*w;
		}
	}
	for(int j=1;j<=ecnt;j++){
		int u=e[j].u,v=e[j].v;
		long double w=e[j].w;
		if(dis[(n+1)&1][v]>dis[(n+1)&1][u]*w)
			return 0;
	}
	for(int i=1;i<=n;i++)
		if(dis[(n+1)&1][i]<=0)return 0;
	return 1;
}
int main(){
	scanf("%d%d%d",&n,&m,&t);
	int mnk=11;
	for(int i=1;i<=m;i++){
		scanf("%d%d%d%d",&OP[i],&A[i],&B[i],&K[i]);
		if(OP[i]==1)mnk=min(mnk,K[i]);
	}
	for(int i=1;i<=t;i++)
		scanf("%d%d",&id[i],&ori[i]),
		giv[id[i]]=1;
	if(check((long double)(0))){
		printf("-1");
		return 0;
	}
	long double l=0,r=mnk;
	while(l+eps<=r){
		long double mid=(l+r)/2;
		if(!check(mid))l=mid;
		else r=mid;
	}
	printf("%.5Lf",l);
	return 0;
}

P3275 [SCOI2011] 糖果

按照朴素差分约束思路图中会出现 \(w=0,w=-1\) 两种情况,先把操作 \(1,3,5\) 按照标准差分约束建图然后缩点成一个 DAG(同强连通分量中的值一定相同),然后考虑 \(2,4\) 可以连边然后直接处理。吗?如果是这样我们还需要写一个负环判定,而 \(n\) 的范围不允许我们这样,考虑类差分约束的写法,把差分约束原图建反向图,然后在上面拓扑排序 + DP,如果存在环那么必然存在没有被取到的节点。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e5+5;
int n,k,A[N],B[N],OP[N],rd[N];
vector<int>G[N];
int scnt,scno[N],low[N],siz[N];
int tp,stk[N],dfn[N],tms;
void tarjan(int u){
	dfn[u]=low[u]=++tms;
	stk[++tp]=u;
	for(int v:G[u]){
		if(!dfn[v]){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else if(!scno[v])low[u]=min(low[u],dfn[v]);
	}
	if(low[u]==dfn[u]){
		int tem=0;
		scnt++;
		do{
			tem=stk[tp--];
			scno[tem]=scnt;
			siz[scnt]++;
		}while(tp&&tem!=u);
	}
}
int head[N],idx;
struct edge{int v,next,w;}e[N<<1];
void con(int x,int y,int z){
	rd[y]++;
	e[++idx].v=y;
	e[idx].next=head[x];
	e[idx].w=z;
	head[x]=idx;
}
int q[N],hd=1,tl,f[N];
LL ans;
int main(){
  ios::sync_with_stdio(0);
  cin.tie(0);cout.tie(0);
  cin>>n>>k;
  for(int i=1;i<=k;i++){
  	cin>>OP[i]>>A[i]>>B[i];
  	if(OP[i]==1||OP[i]==3)
  		G[A[i]].emplace_back(B[i]);
  	if(OP[i]==1||OP[i]==5)
  		G[B[i]].emplace_back(A[i]);
	}
	for(int i=1;i<=n;i++)
		if(!dfn[i])tarjan(i);
	for(int i=1;i<=k;i++){
		int op=OP[i],a=A[i],b=B[i];
		if(op==2||op==4){
			if(scno[a]==scno[b]){
				cout<<'-'<<'1'<<'\n';
				return 0;
			}
			if(op==2)con(scno[a],scno[b],1);
			else con(scno[b],scno[a],1);
		}
		else if(scno[a]!=scno[b]){
			if(op==3)con(scno[b],scno[a],0);
			else if(op==5)con(scno[a],scno[b],0);
		}
	}
	for(int i=1;i<=scnt;i++)
		if(!rd[i])q[++tl]=i,f[i]=1;
	while(hd<=tl){
		int u=q[hd++],mx=0;
		ans+=f[u]*1ll*siz[u];
		for(int i=head[u];i;i=e[i].next){
			int v=e[i].v,w=e[i].w;
			f[v]=max(f[v],f[u]+w);
			rd[v]--;
			if(!rd[v])q[++tl]=v;
		}
	}
	if(tl!=scnt)cout<<'-'<<'1'<<'\n';
	else cout<<ans;
  return 0;
}

P3530 [POI 2012] FES-Festival

先建出所有差分约束边,进行 Tarjan 缩点,同一个强连通分量内(如果合法)点的相对值都是确定的,而每个强连通分量的取值是独立的,可以分开考虑,答案就是每个 SCC 里点跑全源最短路的最大最短路值 \(+1\)

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=605,INF=1e9;
int n,m1,m2;
int tms;
vector<int>G[N];
int scnt,stk[N],tp,dfn[N],low[N],scno[N];
vector<int>P[N];
int dis[N][N];
void tarjan(int u){
	low[u]=dfn[u]=++tms;
	stk[++tp]=u;
	for(int v:G[u]){
		if(!dfn[v]){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else if(!scno[v])low[u]=min(low[u],dfn[v]);
	}
	if(low[u]==dfn[u]){
		int tem=0;
		scnt++;
		do{
			tem=stk[tp--];
			scno[tem]=scnt;
			P[scnt].emplace_back(tem);
		}
		while(tp&&tem!=u);
	}
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m1>>m2;
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			if(i!=j)dis[i][j]=INF;
	for(int i=1;i<=m1;i++){
		int a,b;cin>>a>>b;
		dis[a][b]=min(dis[a][b],1);dis[b][a]=min(dis[b][a],-1);
		G[a].emplace_back(b);
		G[b].emplace_back(a);
	}
	for(int i=1;i<=m2;i++){
		int a,b;cin>>a>>b;
		dis[b][a]=min(dis[b][a],0);
		G[b].emplace_back(a);
	}
	for(int i=1;i<=n;i++)
		if(!dfn[i])tarjan(i);
	int ans=0;
	for(int ID=1;ID<=scnt;ID++){
		for(int k:P[ID])
			for(int i:P[ID])
				for(int j:P[ID]){
					dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
				}
		int mx=0;
		for(int i:P[ID]){
			if(dis[i][i]<0){
				cout<<"NIE";
				return 0;
			}
			for(int j:P[ID])
				if(dis[i][j]<INF)mx=max(mx,dis[i][j]+1);
		}
		ans+=mx;
	}
	cout<<ans;
	return 0;
}

posted @ 2025-08-13 15:31  TBSF_0207  阅读(10)  评论(0)    收藏  举报