【学习笔记】差分约束
前言
2024.1.27
在讲不要忽略算法的细节时,以最短路和差分约束为例子。发现自己差分约束忘得差不多了,于是就有了这篇博客。
负环
- 在一张图中,若存在一条边权之和为负数的回路,则称这个回路为负环。在一张图中,若存在一条边权之和为正数的回路,则称这个回路为正环。
- 如果一张图中存在负环,则可以表现为在
的迭代过程中,无论经过多少次迭代,总存在一条有向边 满足 ,其中 为起点,从而使得 的迭代永远不能结束。 - 代码实现
- 判定最短路径包含的边数
-
设
表示从起点 到 的最短路径包含的边数。规定 。当执行 时,同时更新 。此时如果有 ,则说明图中存在从起点 出发能到达的负环。若迭代能够完成,即算法正常结束,则说明图中不存在从起点 出发能到达的负环。点击查看代码
bool spfa(int s,int n) { int i,x; memset(vis,0,sizeof(vis)); memset(dis,0x3f,sizeof(dis)); queue<int>q; q.push(s); dis[s]=0; vis[s]=1; while(q.empty()==0) { x=q.front(); vis[x]=0; q.pop(); for(i=head[x];i!=0;i=e[i].nxt) { if(dis[e[i].to]>dis[x]+e[i].w) { dis[e[i].to]=dis[x]+e[i].w; num[e[i].to]=num[x]+1; if(num[e[i].to]>=n) { return false;//若返回false,则说明图中存在从起点s出发能到达的负环 } if(vis[e[i].to]==0) { q.push(e[i].to); vis[e[i].to]=1; } } } } return true;//若返回true,则说明图中不存在从起点s出发能到达的负环 }
-
- 卡时做法
-
对于上面的判定最短路径包含的边数做法,可以根据运行时间限制,给
设定一个上界,超出时直接判定存在负环。点击查看代码
......//剩余代码同上 if(num[e[i].to]>=MAX_N)//MAX_N为设定的上界 { return false;//若返回false,则说明图中存在从起点s出发能到达的负环 } ......//剩余代码同上
-
- 将
从基于 实现改为基于 实现-
对于一条有向边
,当执行 时,同时判断 是否已经访问过或从 出发是否能到达负环。对于前者,如果 在递归前已经被访问过,则说明图中存在从起点 出发能到达的负环;否则,则说明图中不存在从起点 出发能到达的负环。对于后者,直接转移即可。点击查看代码
bool spfa(int s) { vis[s]=1; for(int i=head[s];i!=0;i=e[i].next) { if(dis[e[i].to]>dis[s]+e[i].w) { dis[e[i].to]=dis[s]+e[i].w; if(vis[e[i].to]==1||spfa(e[i].to)==false) { return false;//若返回false,则说明图中存在从起点s出发能到达的负环 } } } vis[s]=0;//记得回溯 return true;//若返回true,则说明图中不存在从起点s出发能到达的负环 } memset(dis,0x3f,sizeof(dis)); memset(vis,0,sizeof(vis)); dis[s]=0;
-
- 判定最短路径包含的边数
- 例题
差分约束
- 差分约束系统是由
个 元一次不等式组成的 元一次不等式组,形如 ,其中对于每一个 均有 。我们要解决的问题是:求一组解 ,使得所有的不等式均成立或给出无解信息。 - 代码实现
- 对于第
个不等式 ,可以变形为 。这与三角形不等式 十分相似。因此我们可以把变量 看做有向图中的一个节点 ,从节点 向节点 连一条 的有向边。- 由不等式基本性质,容易有若
是原差分约束系统的一组解,则 也是原差分约束系统的一组解,其中 为常数。 - 如果题目要求给定了所有解的最小值
或通过如上的构图方式所构出的图不连通时,需要加入一个超级源点 ,并令 ,同时原差分约束系统增加了 个 元一次不等式 。
- 由不等式基本性质,容易有若
- 设
,以 为起点(超级源点)求单源最短路。若图中存在负环,则给定的差分约束系统无解;否则, 为原差分约束系统的一组最大解。- 一些比较显然的转化
- 若对于任意一个
有 可以从节点 向节点 连一条 的有向边,求单源最长路,判定有无解变为是否存在正环,此时得到的解为原差分约束系统的一组最小解;或者可以转化为 。- 同样地,若对于任意一个
有 也可以转化为 。
- 同样地,若对于任意一个
- 若对于任意一个
有 可以转化为 ,即 。 - 若对于任意一个
有 可以转化为 。 - 若对于任意一个
有 可以转化为 。
- 若对于任意一个
- 一些比较显然的转化
- 对于第
- 例题
- luogu P5960 【模板】差分约束
-
以下两种不同的写法均可。
单源最短路找负环
void spfa(ll s,ll n)//单源最短路找负环 { ll i,x; memset(vis,0,sizeof(vis)); memset(dis,0x3f,sizeof(dis)); queue<ll>q; q.push(s); dis[s]=0; vis[s]=1; while(q.empty()==0) { x=q.front(); vis[x]=0; q.pop(); for(i=head[x];i!=0;i=e[i].nxt) { if(dis[e[i].to]>dis[x]+e[i].w) { dis[e[i].to]=dis[x]+e[i].w; num[e[i].to]=num[x]+1; if(num[e[i].to]>=n+1) { cout<<"NO"<<endl; exit(0); } if(vis[e[i].to]==0) { q.push(e[i].to); vis[e[i].to]=1; } } } } } int main() { ll n,m,u,v,w,i; cin>>n>>m; for(i=1;i<=n;i++) { add(0,i,0); } for(i=1;i<=m;i++) { cin>>u>>v>>w; add(v,u,w); } spfa(0,n); for(i=1;i<=n;i++) { cout<<dis[i]<<" "; } return 0; }
单源最长路找正环
void spfa(ll s,ll n) { ll i,x; memset(vis,0,sizeof(vis)); memset(dis,-0x3f,sizeof(dis)); queue<ll>q; q.push(s); dis[s]=0; vis[s]=1; while(q.empty()==0) { x=q.front(); vis[x]=0; q.pop(); for(i=head[x];i!=0;i=e[i].nxt) { if(dis[e[i].to]<dis[x]+e[i].w) { dis[e[i].to]=dis[x]+e[i].w; num[e[i].to]=num[x]+1; if(num[e[i].to]>=n+1) { cout<<"NO"<<endl; exit(0); } if(vis[e[i].to]==0) { q.push(e[i].to); vis[e[i].to]=1; } } } } } int main() { ll n,m,u,v,w,i; cin>>n>>m; for(i=1;i<=n;i++) { add(0,i,0); } for(i=1;i<=m;i++) { cin>>u>>v>>w; add(u,v,-w); } spfa(0,n); for(i=1;i<=n;i++) { cout<<dis[i]<<" "; } return 0; }
-
- luogu P1260 工程规划
- tgHZOJ 183. 小K的农场
- 数据弱化版:BZOJ 3436.小K的农场 | luogu P1993 小 K 的农场
- 本题因数据经过特殊的构造,卡掉了基于
实现的 。故需要使用deque
优化 或使用 式的 。但是因为数据较水,使用卡时做法也可通过。
- luogu P1993 小 K 的农场 经过特殊的构造,用遍历边的顺序卡掉了链式前向星的
式的 ,需要使用vector
建图或事先打乱边。
- luogu P1993 小 K 的农场 经过特殊的构造,用遍历边的顺序卡掉了链式前向星的
- AT_arc090_b [ABC087D] People on a Line
-
不知道为啥空间开到
才过。点击查看代码
struct node { int nxt,to,w; }e[4000010]; int head[4000010],dis[4000010],vis[4000010],num[4000010],cnt=0; void add(int u,int v,int w) { cnt++; e[cnt].nxt=head[u]; e[cnt].to=v; e[cnt].w=w; head[u]=cnt; } bool spfa(int s) { vis[s]=1; for(int i=head[s];i!=0;i=e[i].nxt) { if(dis[e[i].to]<dis[s]+e[i].w) { dis[e[i].to]=dis[s]+e[i].w; if(vis[e[i].to]==1||spfa(e[i].to)==false) { return false; } } } vis[s]=0; return true; } int main() { int n,m,u,v,w,i; cin>>n>>m; for(i=1;i<=m;i++) { cin>>u>>v>>w; add(u,v,w); add(v,u,-w); } for(i=1;i<=n;i++) { add(0,i,0); } memset(dis,-0x3f,sizeof(dis)); memset(vis,0,sizeof(vis)); dis[0]=0; if(spfa(0)==true) { cout<<"Yes"<<endl; } else { cout<<"No"<<endl; } return 0; }
-
- luogu P3275 [SCOI2011] 糖果
-
容易发现,单源最长路求正环时,边权只有
和 。故若图中存在环,则这个环边权之和必须等于 ,否则一定无解。 -
类似 luogu P3008 [USACO11JAN] Roads and Planes G ,我们对原图进行缩点。然后进行拓扑求最长路即可。
点击查看代码
struct node { ll nxt,to,w; }e[300000]; ll head[300000],dis[300000],dfn[300000],low[300000],ins[300000],scc[300000],c[300000],u[300000],v[300000],w[300000],din[300000],cnt=0,m=0,tot=0,ans=0; stack<ll>s; void add1(ll u,ll v,ll w) { cnt++; e[cnt].nxt=head[u]; e[cnt].to=v; e[cnt].w=w; head[u]=cnt; } void add2(ll uu,ll vv,ll ww) { m++; u[m]=uu; v[m]=vv; w[m]=ww; } void tarjan(ll x) { int i,k=0; tot++; dfn[x]=low[x]=tot; ins[x]=1; s.push(x); for(i=head[x];i!=0;i=e[i].nxt) { if(dfn[e[i].to]==0) { tarjan(e[i].to); low[x]=min(low[x],low[e[i].to]); } else { if(ins[e[i].to]==1) { low[x]=min(low[x],dfn[e[i].to]); } } } if(dfn[x]==low[x]) { ans++; while(x!=k) { k=s.top(); ins[k]=0; c[k]=ans; scc[ans]++; s.pop(); } } } void top_sort(ll n) { queue<ll>q; ll i,x; for(i=1;i<=n;i++) { if(din[i]==0) { q.push(i); dis[i]=1; } } while(q.empty()==0) { x=q.front(); q.pop(); for(i=head[x];i!=0;i=e[i].nxt) { dis[e[i].to]=max(dis[e[i].to],dis[x]+e[i].w); din[e[i].to]--; if(din[e[i].to]==0) { q.push(e[i].to); } } } } int main() { ll n,q,uu,vv,pd,sum=-1,flag=0,i;//因为额外多算了n+1,所以事先需要设为-1 cin>>n>>q; for(i=1;i<=n;i++) { add1(n+1,i,0); add2(n+1,i,0); } for(i=1;i<=q;i++) { cin>>pd>>uu>>vv; switch(pd) { case 1: add1(uu,vv,0); add2(uu,vv,0); add1(vv,uu,0); add2(vv,uu,0); break; case 2: add1(uu,vv,1); add2(uu,vv,1); break; case 3: add1(vv,uu,0); add2(vv,uu,0); break; case 4: add1(vv,uu,1); add2(vv,uu,1); break; case 5: add1(uu,vv,0); add2(uu,vv,0); break; } } for(i=1;i<=n+1;i++) { if(dfn[i]==0) { tarjan(i); } } cnt=0; memset(e,0,sizeof(e)); memset(head,0,sizeof(dis)); for(i=1;i<=m;i++) { if(c[u[i]]!=c[v[i]]) { add1(c[u[i]],c[v[i]],w[i]); din[c[v[i]]]++; } else { if(w[i]==1) { flag=1; break; } } } if(flag==0) { top_sort(ans); for(i=1;i<=ans;i++) { sum+=dis[i]*scc[i]; } } cout<<sum<<endl; return 0; }
-
- luogu P4878 [USACO05DEC] Layout G
-
设
表示 中奶牛的数量。 -
由于一个点可以有多头奶牛,隐含着
的关系。 -
无法排队即建出的图中存在负环(需要从超级源点开始遍历)。
-
可以任意远离即
无法到达 。点击查看代码
struct node { int nxt,to,w; }e[100000]; int head[100000],dis[100000],vis[100000],num[100000],cnt=0; void add(int u,int v,int w) { cnt++; e[cnt].nxt=head[u]; e[cnt].to=v; e[cnt].w=w; head[u]=cnt; } void spfa(int s,int n) { memset(dis,0x3f,sizeof(dis)); memset(vis,0,sizeof(vis)); memset(num,0,sizeof(num)); int x,i; queue<int>q; q.push(s); dis[s]=0; vis[s]=1; while(q.empty()==0) { x=q.front(); vis[x]=0; q.pop(); for(i=head[x];i!=0;i=e[i].nxt) { if(dis[e[i].to]>dis[x]+e[i].w) { dis[e[i].to]=dis[x]+e[i].w; num[e[i].to]=num[x]+1; if(num[e[i].to]>=n+1) { cout<<"-1"<<endl; exit(0); } if(vis[e[i].to]==0) { q.push(e[i].to); vis[e[i].to]=1; } } } } } int main() { int n,m1,m2,i,u,v,w; cin>>n>>m1>>m2; for(i=1;i<=m1;i++) { cin>>u>>v>>w; add(u,v,w); } for(i=1;i<=m2;i++) { cin>>u>>v>>w; add(v,u,-w); } for(i=1;i<=n;i++) { add(0,i,0); } for(i=1;i<=n-1;i++) { add(i+1,i,-0); } spfa(0,n); spfa(1,n); if(dis[n]==0x3f3f3f3f) { cout<<"-2"<<endl; } else { cout<<dis[n]<<endl; } return 0; }
-
- luogu P1645 序列
-
多倍经验: luogu P1250 种树 | luogu P1986 元旦晚会 | SP116 INTERVAL - Intervals | UVA1723 Intervals | luogu P10934 西瓜种植
-
设
表示 中选择的数的个数。 -
中至少有 个数被选出等价于 。 -
由于一个数只能选一次,隐含着
的关系。点击查看代码
struct node { int nxt,to,w; }e[200001]; int head[200001],vis[200001],dis[200001],cnt=0; void add(int u,int v,int w) { cnt++; e[cnt].nxt=head[u]; e[cnt].to=v; e[cnt].w=w; head[u]=cnt; } void spfa(int s) { int i,x; memset(vis,0,sizeof(vis)); memset(dis,-0x3f,sizeof(dis)); queue<int>q; q.push(s); dis[s]=0; vis[s]=1; while(q.empty()==0) { x=q.front(); vis[x]=0; q.pop(); for(i=head[x];i!=0;i=e[i].nxt) { if(dis[e[i].to]<dis[x]+e[i].w) { dis[e[i].to]=dis[x]+e[i].w; if(vis[e[i].to]==0) { q.push(e[i].to); vis[e[i].to]=1; } } } } } int main() { int m,n=0,u,v,w,i,t,j; cin>>m; for(i=1;i<=m;i++) { cin>>u>>v>>w; add(u,v+1,w); n=max(n,v+1); } for(i=1;i<=n;i++) { add(i-1,i,0); add(i,i-1,-1); } spfa(0); cout<<dis[n]<<endl; return 0; }
-
- luogu P10941 Cashier Employment
-
为方便代码书写,规定时间为
。 -
设
表示应聘的人中从第 个小时开始工作的人的数量, 表示招聘的人中从第 个小时开始工作的人的数量。 -
由题,有
,即 ,移项得 。 -
由于一个数只能选一次,隐含着
,即 。 -
枚举
判断即可。点击查看代码
struct node { int nxt,to,w; }e[100]; int head[100],dis[100],vis[100],num[100],r[100],sum[100],cnt=0; void add(int u,int v,int w) { cnt++; e[cnt].nxt=head[u]; e[cnt].to=v; e[cnt].w=w; head[u]=cnt; } bool spfa(int s,int n) { memset(dis,-0x3f,sizeof(dis)); memset(vis,0,sizeof(vis)); memset(num,0,sizeof(num)); int x,i; queue<int>q; q.push(s); dis[s]=0; vis[s]=1; while(q.empty()==0) { x=q.front(); vis[x]=0; q.pop(); for(i=head[x];i!=0;i=e[i].nxt) { if(dis[e[i].to]<dis[x]+e[i].w) { dis[e[i].to]=dis[x]+e[i].w; num[e[i].to]=num[x]+1; if(num[e[i].to]>=n+1) { return false; } if(vis[e[i].to]==0) { q.push(e[i].to); vis[e[i].to]=1; } } } } return true; } int main() { int t,n,x,ans,i,j,k; cin>>t; for(k=1;k<=t;k++) { memset(sum,0,sizeof(sum)); ans=-1; for(i=1;i<=24;i++) { cin>>r[i]; } cin>>n; for(i=1;i<=n;i++) { cin>>x; sum[x+1]++; } for(i=0;i<=n;i++) { cnt=0; memset(e,0,sizeof(e)); memset(head,0,sizeof(head)); add(0,24,i); add(24,0,-i); for(j=1;j<=24;j++) { add(j-1,j,0); add(j,j-1,-sum[j]); } for(j=1;j<=8;j++) { add(j+16,j,r[j]-i); } for(j=9;j<=24;j++) { add(j-8,j,r[j]); } if(spfa(0,24)==true) { ans=i; break; } } if(ans!=-1) { cout<<ans<<endl; } else { cout<<"No Solution"<<endl; } } return 0; }
-
- luogu P5960 【模板】差分约束
参考资料
《算法竞赛进阶指南》——李煜东
本文来自博客园,作者:hzoi_Shadow,原文链接:https://www.cnblogs.com/The-Shadow-Dragon/p/17991944,未经允许严禁转载。
版权声明:本作品采用 「署名-非商业性使用-相同方式共享 4.0 国际」许可协议(CC BY-NC-SA 4.0) 进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现