(笔记)差分约束
为什么我在记别人两年前学过的东西??
思想
若干个变量构成了相互 \(m\) 条约束形如:
显然可以转化为 \((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;
}

浙公网安备 33010602011771号