前言
2024.1.27 \(huge\) 在讲不要忽略算法的细节时,以最短路和差分约束为例子。发现自己差分约束忘得差不多了,于是就有了这篇博客。
负环
-
在一张图中,若存在一条边权之和为负数的回路,则称这个回路为负环。在一张图中,若存在一条边权之和为正数的回路,则称这个回路为正环。
-
如果一张图中存在负环,则可以表现为在 \(SPFA\) 的迭代过程中,无论经过多少次迭代,总存在一条有向边 \((x,y,z)\) 满足 \(dis_{s,y}>dis_{s,x}+z\) ,其中 \(s\) 为起点,从而使得 \(SPFA\) 的迭代永远不能结束。
-
代码实现
-
判定最短路径包含的边数
-
设 \(cnt_{s,x}\) 表示从起点 \(s\) 到 \(x\) 的最短路径包含的边数。规定 \(cnt_{s,s}=0\) 。当执行 \(dis_{s,y}=dis_{s,x}+z\) 时,同时更新 \(cnt_{s,y}=cnt_{s,x}+1\) 。此时如果有 \(cnt_{s,y} \ge n\) ,则说明图中存在从起点 \(s\) 出发能到达的负环。若迭代能够完成,即算法正常结束,则说明图中不存在从起点 \(s\) 出发能到达的负环。
点击查看代码
cppbool 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出发能到达的负环 }
-
-
卡时做法
-
对于上面的判定最短路径包含的边数做法,可以根据运行时间限制,给 \(cnt_{y}\) 设定一个上界,超出时直接判定存在负环。
点击查看代码
cpp......//剩余代码同上 if(num[e[i].to]>=MAX_N)//MAX_N为设定的上界 { return false;//若返回false,则说明图中存在从起点s出发能到达的负环 } ......//剩余代码同上
-
-
将 \(SPFA\) 从基于 \(BFS\) 实现改为基于 \(DFS\) 实现
-
对于一条有向边 \((x,y,z)\) ,当执行 \(dis_{s,y}=dis_{s,x}+z\) 时,同时判断 \(y\) 是否已经访问过或从 \(y\) 出发是否能到达负环。对于前者,如果 \(y\) 在递归前已经被访问过,则说明图中存在从起点 \(s\) 出发能到达的负环;否则,则说明图中不存在从起点 \(s\) 出发能到达的负环。对于后者,直接转移即可。
点击查看代码
cppbool 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;
-
-
-
例题
差分约束
- 差分约束系统是由 \(m\) 个 \(n\) 元一次不等式组成的 \(n\) 元一次不等式组,形如 \(\begin{cases}x_{a_{1}}-x_{b_{1}} \le c_{1}\\x_{a_{2}}-x_{b_{2}} \le c_{2} \\ \dots \\ x_{a_{m}}-x_{b_{m}} \le c_{m}\end{cases}\) ,其中对于每一个 \(i(1 \le i \le n)\) 均有 \(1 \le a_{i},b_{i} \le n\) 。我们要解决的问题是:求一组解 \(\begin{cases}x_{1}=ans_{1} \\ x_{2}=ans_{2} \\ \dots \\ x_{n}=ans_{n}\end{cases}\) ,使得所有的不等式均成立或给出无解信息。
- 代码实现
- 对于第 \(i(1 \le i \le m)\) 个不等式 \(x_{a_{i}}-x_{b_{i}} \le c_{i}\) ,可以变形为 \(x_{a_{i}} \le x_{b_{i}}+c_{i}\) 。这与三角形不等式 \(dis_{s,y} \le dis_{s,x}+z\) 十分相似。因此我们可以把变量 \(x_{a_{i}}\) 看做有向图中的一个节点 \(a_{i}\) ,从节点 \(b_{i}\) 向节点 \(a_{i}\) 连一条 \(c_{i}\) 的有向边。
- 由不等式基本性质,容易有若 \(\begin{cases}x_{1}=ans_{1} \\ x_{2}=ans_{2} \\ \dots \\ x_{n}=ans_{n}\end{cases}\) 是原差分约束系统的一组解,则 \(\begin{cases}x_{1}=ans_{1}+d \\ x_{2}=ans_{2}+d \\ \dots \\ x_{n}=ans_{n}+d\end{cases}\) 也是原差分约束系统的一组解,其中 \(d\) 为常数。
- 如果题目要求给定了所有解的最小值 \(k\) 或通过如上的构图方式所构出的图不连通时,需要加入一个超级源点 \(0\) ,并令 \(x_{0}=k\) ,同时原差分约束系统增加了 \(n\) 个 \(n\) 元一次不等式 \(x_{a_{i}}-x_{0} \le k\) 。
- 设 \(dis_{0,0}=k\) ,以 \(0\) 为起点(超级源点)求单源最短路。若图中存在负环,则给定的差分约束系统无解;否则, \(\begin{cases}x_{1}=dis_{1} \\ x_{2}=dis_{2} \\ \dots \\ x_{n}=dis_{n}\end{cases}\) 为原差分约束系统的一组解。
- 一些比较显然的转化
- 若对于任意一个 \(i(1 \le i \le m)\) 有 \(x_{a_{i}}-x_{b_{i}} \ge c_{i}\) 可以从节点 \(b_{i}\) 向节点 \(a_{i}\) 连一条 \(c_{i}\) 的有向边,求单源最长路,判定有无解变为是否存在正环;或者可以转化为 \(x_{b_{i}}-x_{a_{i}} \le -c_{i}\) 。
- 同样地,若对于任意一个 \(i\) 有 \(x_{a_{i}}-x_{b_{i}} \le c_{i}\) 也可以转化为 \(x_{b_{i}}-x_{a_{i}} \ge -c_{i}\) 。
- 若对于任意一个 \(i(1 \le i \le m)\) 有 \(x_{a_{i}}-x_{b_{i}}=c_{i}\) 可以转化为 \(\begin{cases}x_{a_{i}}-x_{b_{i}} \le c_{i} \\ x_{a_{i}}-x_{b_{i}} \ge c_{i}\end{cases}\) 。
- 若对于任意一个 \(i(1 \le i \le m)\) 有 \(x_{a_{i}}-x_{b_{i}}>c_{i}\) 可以转化为 \(x_{a_{i}}-x_{b_{i}} \ge c_{i}+1\) 。
- 若对于任意一个 \(i(1 \le i \le m)\) 有 \(x_{a_{i}}-x_{b_{i}}<c_{i}\) 可以转化为 \(x_{a_{i}}-x_{b_{i}} \le c_{i}-1\) 。
- 若对于任意一个 \(i(1 \le i \le m)\) 有 \(x_{a_{i}}-x_{b_{i}} \ge c_{i}\) 可以从节点 \(b_{i}\) 向节点 \(a_{i}\) 连一条 \(c_{i}\) 的有向边,求单源最长路,判定有无解变为是否存在正环;或者可以转化为 \(x_{b_{i}}-x_{a_{i}} \le -c_{i}\) 。
- 一些比较显然的转化
- 对于第 \(i(1 \le i \le m)\) 个不等式 \(x_{a_{i}}-x_{b_{i}} \le c_{i}\) ,可以变形为 \(x_{a_{i}} \le x_{b_{i}}+c_{i}\) 。这与三角形不等式 \(dis_{s,y} \le dis_{s,x}+z\) 十分相似。因此我们可以把变量 \(x_{a_{i}}\) 看做有向图中的一个节点 \(a_{i}\) ,从节点 \(b_{i}\) 向节点 \(a_{i}\) 连一条 \(c_{i}\) 的有向边。
- 例题
-
-
以下两种不同的写法均可。
单源最短路找负环
cppvoid 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; }
单源最长路找正环
cppvoid 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; }
-
-
- 数据弱化版:BZOJ 3436.小K的农场 | luogu P1993 小 K 的农场
- 本题因数据经过特殊的构造,卡掉了基于 \(BFS\) 实现的 \(SPFA\) 。故需要使用
deque
优化 \(SPFA\) 或使用 \(DFS\) 式的 \(SPFA\) 。但是因为数据较水,使用卡时做法也可通过。
- luogu P1993 小 K 的农场 经过特殊的构造,用遍历边的顺序卡掉了链式前向星的 \(DFS\) 式的 \(SPFA\) ,需要使用
vector
建图或事先打乱边。
- luogu P1993 小 K 的农场 经过特殊的构造,用遍历边的顺序卡掉了链式前向星的 \(DFS\) 式的 \(SPFA\) ,需要使用
-
AT_arc090_b [ABC087D] People on a Line
-
不知道为啥空间开到 \(20 \times m\) 才过。
点击查看代码
cppstruct 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; }
-
-
-
容易发现,单源最长路求正环时,边权只有 \(0\) 和 \(1\) 。故若图中存在环,则这个环边权之和必须等于 \(0\) ,否则一定无解。
-
类似 luogu P3008 [USACO11JAN] Roads and Planes G ,我们对原图进行缩点。然后进行拓扑求最长路即可。
点击查看代码
cppstruct 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
-
设 \(x_{i}\) 表示 \(0 \sim i\) 中奶牛的数量。
-
由于一个点可以有多头奶牛,隐含着 \(x_{i+1}-x_{i} \ge 0\) 的关系。
-
无法排队即建出的图中存在负环(需要从超级源点开始遍历)。
-
可以任意远离即 \(1\) 无法到达 \(n\) 。
点击查看代码
cppstruct 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 P1250 种树 | luogu P1986 元旦晚会 | SP116 INTERVAL - Intervals | UVA1723 Intervals
-
设 \(x_{i}\) 表示 \(0 \sim i\) 中选择的数的个数。
-
\([a_{i},b_{i}]\) 中至少有 \(c_{i}\) 个数被选出等价于 \(x_{b_{i}+1}-x_{a_{i}} \ge c_{i}\) 。
-
由于一个数只能选一次,隐含着 \(0 \le x_{i}-x_{i-1} \le 1\) 的关系。
点击查看代码
cppstruct 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; }
-
-
LibreOJ 10088. 「一本通 3.4 例 2」出纳员问题
-
为方便代码书写,规定时间为 \(1 \sim 24\) 。
-
设 \(sum_{i}\) 表示应聘的人中从第 \(i\) 个小时开始工作的人的数量, \(x_{i}\) 表示招聘的人中从第 \(i\) 个小时开始工作的人的数量。
-
由题,有 \(\begin{cases} \sum\limits_{j=1}^{i}x_{j}+\sum\limits_{17+i}^{24}x_{j} \ge r_{i} & 1 \le i \le 7 \\ \sum\limits_{j=i-7}^{i}x_{j} \ge r_{i} & 8 \le i \le 24 \end{cases}\) ,即 \(\begin{cases} \sum\limits_{j=1}^{i}x_{j}+\sum\limits_{j=1}^{24}x_{j}-\sum\limits_{j=1}^{16+i}x_{j} \ge r_{i} & 1 \le i \le 7 \\ \sum\limits_{j=1}^{i}x_{j}-\sum\limits_{j=1}^{i-8}x_{j} \ge r_{i} & 8 \le i \le 24 \end{cases}\) ,移项得 \(\begin{cases} \sum\limits_{j=1}^{i}x_{j}-\sum\limits_{j=1}^{16+i}x_{j} \ge r_{i}-\sum\limits_{j=1}^{24}x_{j} & 1 \le i \le 7 \\ \sum\limits_{j=1}^{i}x_{j}-\sum\limits_{j=1}^{i-8}x_{j} \ge r_{i} & 8 \le i \le 24 \end{cases}\) 。
-
由于一个数只能选一次,隐含着 \(0 \le x_{i}-x_{i-1} \le sum_{i}\) ,即 \(0 \le \sum\limits_{j=1}^{i}x_{j}-\sum\limits_{j=1}^{i-1}x_{j} \le sum_{i}\) 。
-
枚举 \(\sum\limits_{i=1}^{24}x_{i}\) 判断即可。
点击查看代码
cppstruct 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; }
-
-
参考资料
《算法竞赛进阶指南》------李煜东