负环与差分约束系统 学习笔记

djwj233

2021-10-07 14:25:10

Personal

## 负环 在一张带权图 $G$ 中,若有一个环满足**环上边权之和为负**,则称这个环为**负环**。 可以发现,带负环的图不存在最短路,因为可以构造一条路径不停地绕着这个环走使总长度为 $-\infty$。 一般可以用 Bellman-Ford 或 SPFA 判负环,其中 SPFA 在随机图上表现出色,但二者的最劣时间复杂度都是 $\Theta(nm)$ 的。 - [P3385 【模板】负环](https://www.luogu.com.cn/problem/P3385) 我们发现,应用 Bellman-Ford 算法时,负环不存在时 $1$ 号点到任意点的最短路包含的边数不超过 $n-1$(否则肯定会重复经过某个点)。 因此可以在做完 $n-1$ 轮松弛后再遍历一遍所有边,若依然能松弛则表示有负环存在。 核心代码: ```c++ int n,m,dis[N]; bool vis[N]; struct edge { int u,v,w; } e[M]; int tot; void solve() { cin>>n>>m; tot=0; fo(i,1,m) { tot++, scanf("%d%d%d",&e[tot].u,&e[tot].v,&e[tot].w); if(e[tot].w>=0) { tot++, e[tot].u=e[tot-1].v; e[tot].v=e[tot-1].u, e[tot].w=e[tot-1].w; } } memset(dis,0x3f,sizeof(dis)), memset(vis,0,sizeof(vis)); dis[1]=0, vis[1]=true; fo(i,2,n) { bool ok=true; fo(j,1,tot) if(dis[e[j].v]>dis[e[j].u]+e[j].w) { ok=false, dis[e[j].v]=dis[e[j].u]+e[j].w; if(vis[e[j].u]) vis[e[j].v]=true; } if(ok) break; } fo(j,1,tot) if(vis[e[j].u]&&vis[e[j].v]&&dis[e[j].v]>dis[e[j].u]+e[j].w) return puts("YES"), void(); puts("NO"); } ``` 对应到 SPFA 算法上,则需要判断一个点入队次数是否 $\geq n$,若 $\geq n$ 则表示有负环存在。 还有一种方法是记录每个点的最短路长度,这种方法一般效率更高。 ## 差分约束系统 ### 概念 **差分约束系统(system of difference constraints)**是一种特殊的 $n$ 元一次不等式组,里面的每一个条件都是 $$ x_i-x_j \le c_k $$ 的形式,也就是**两变量之差不大于某数**。(若要求不小于可以把 $i,j$ 调换顺序) 问题要求给出一组合法解,或者判断它无解。 ### 模型的转换 可以移项得到: $$ x_i\le x_j+c_k $$ 这正是最短路中的**三角形不等式**的形式: $$ \forall (u,v,w)\in E,\ dis_v\le dis_u+w $$ 那么我们可以尝试建图,对每个不等式连边 $(j,i,c_k)$,然后考虑跑最短路。 但是这个图不一定是一个流图,我们难以找出一个源点跑单源最短路,于是我们可以新建一个虚拟源点 $0$,对每个 $i\in[1,n]$ 连边 $(0,i,0)$。 现在我们以 $0$ 号点为源点跑一个单源最短路,跑出来每个点的最短路 $dis_i$ 一定是一组合法的解。 那么无解的情况呢?我们发现,差分约束系统无解当且仅当建出的图中不存在最短路,也即图中有负环。 这样我们就能以 $\mathcal O(n+m)$ 的时间解决这一问题了。 差分约束系统有一个平凡的性质: > 若 $\{x_1,x_2,\cdots,x_n\}$ 是差分约束系统的一组合法解,则 $\forall \Delta\in\mathbb R$,$\{x_1+\Delta,x_2+\Delta,\cdots,x_n+\Delta\}$ 也是一组合法解。 这个结论是显然的。若 $x_i$ 中有负数,使用中常取 $\Delta=-\min{x_i}$,可构造出一组非负解。 - [P5960 【模板】差分约束算法](https://www.luogu.com.cn/problem/P5960) 这里使用 SPFA 算法求最短路。 ```c++ #include<bits/stdc++.h> using namespace std; #define fo(v,a,b) for(int v=a;v<=b;v++) #define fr(v,a,b) for(int v=a;v>=b;v--) #define il inline const int N=5010, M=20010; int n,m; int head[N], Next[M], ver[M], edge[M], tot; void add_edge(int x,int y,int z) { ver[++tot]=y, edge[tot]=z; Next[tot]=head[x], head[x]=tot; } int dis[N], cnt[N]; bool vis[N]; queue<int> q; int main() { cin>>n>>m; fo(i,1,m) { int x,y,z; scanf("%d%d%d",&x,&y,&z); add_edge(y,x,z); } fo(i,1,n) add_edge(0,i,0); memset(dis,0x3f,sizeof(dis)); dis[0]=0, vis[0]=true, q.push(0); while(q.size()) { int x=q.front(); q.pop(); vis[x]=false; for(int i=head[x]; i; i=Next[i]) { int y=ver[i]; if(dis[y]>dis[x]+edge[i]) { dis[y]=dis[x]+edge[i]; cnt[y]=cnt[x]+1; if(cnt[y]>=n+1) return puts("NO"),0; if(!vis[y]) vis[y]=true, q.push(y); } } } fo(i,1,n) printf("%d ",dis[i]); return 0; } ``` 注意因为实际上有 $n+1$ 个点,所以里面判负环需要写成 $\geq n+1$。 还有注意存边的数组不要开小!!! ## 常见建模思路 主要难在建模...... 而且有些差分约束的题目还可以用贪心做,因此大多数差分约束的题目第一眼看是贪心( 总之就是看着十分难搞的**二元约束性**问题都可以考虑差分约束,最主要的点还是**二元**吧。 ### Trick 1 - 等号化为不等号 **例题:**[P4578 [FJOI2018]所罗门王的宝藏](https://www.luogu.com.cn/problem/P4578) 我们设 $row_i$ 为第 $i$ 行的转动结果,$col_i$ 为第 $i$ 列的转动结果。那么对于每个限制 $(x,y,c)$ 有: $$ row_i+col_i=c $$ 好像不太好搞?实际上,原式就是: $$ row_i-(-col_i)=c $$ **(重点)既然有差分约束的影子了,那么就可以把这个等号拆成两个约束:** $$ \begin{cases} row_i-(-col_i)\le c \\ (-col_i)-row_i\le -c \end{cases} $$ 也就是说**等号相当于连反向、反权边**。 [Code](https://www.luogu.com.cn/record/59411044) ### Trick 2 - 前缀和形式 **例题:**[P2294 [HNOI2005]狡猾的商人](https://www.luogu.com.cn/problem/P2294) 这个前缀和的提示还算比较明显的,有些题目可能更为隐晦。 一般题目给出的约束是**区间上一整段 至少/至多 的价值**,就可以考虑前缀和形式。 设 $S_i=\sum_{k=1}^i a_i$,那么每个限制条件 $(s,t,v)$ 可被写作: $$ S_t-S_{s-1}\le v $$ $$ S_{s-1}-S_t\le -v $$ 然后就可以搞了。注意这里 $s-1$ 能取到 $0$,因此可以把源点设成 $n+1$。 [Code](https://www.luogu.com.cn/record/59413480) ### Trick 3 - 结合最短路和最长路 **例题:**[P2474 [SCOI2008]天平](https://www.luogu.com.cn/problem/P2474)(难) 考虑到里面要求**必定成立**,因此我们考虑跑出每个组差的上界和下界。 我们注意到对于任意 $x,y$,由于 $a_x,a_y\in[1,3]$,因此 $-2\le a_x-a_y\le2$,这便是所有字符需要满足的条件。 `+`:$a_x>a_y\Rightarrow a_x-a_y\geq 1$,有 $a_x\geq a_y+1$。 `-`:$a_x<a_y\Rightarrow a_x-a_y\le -1$,有 $a_x\leq a_y-1$。 `=`:$a_x=a_y\Rightarrow a_x\le a_y\land a_y\le a_x$。 `?`:$a_x\geq a_y-2\land a_x\le a_y+2$。 因此我们注意到**对同一个 $a_x$,既有 $\le$ 的限制条件,又有 $\ge$ 的限制条件**。 这里如果简单地取负数来把不等号反向,会多出来一个变量 $-a_x$,而它与 $a_x$ 是绑定死的,难以作为一个新变量。 这时我们可以考虑**同时求最短路、最长路**来把问题转化成类似于差分约束的形式。 建图部分: ```c++ fo(i,1,n) fo(j,1,n) dis0[i][j]=dis1[i][j]=114514; fo(i,1,n) { scanf("%s",s+1); fo(j,1,n) { if(s[j]=='+') { dis0[i][j]=1, dis1[i][j]=2, dis0[j][i]=-2, dis1[j][i]=-1; } else if(s[j]=='-') { dis0[i][j]=-2, dis1[i][j]=-1, dis0[j][i]=1, dis1[j][i]=2; } else if(s[j]=='=') { dis0[i][j]=dis1[i][j]=dis0[j][i]=dis1[j][i]=0; } else if(dis0[j][i]==114514) { dis0[j][i]=-2, dis1[j][i]=2; } } } ``` 由于要求出每个点对的差值,因此跑一个 Floyd 求出最短路和最长路,暴力枚举统计答案。 (这里的 $\texttt{dis0[i][j]}$ 表示 $x_i-x_j$ 的最小值, $\texttt{dis1[i][j]}$ 表示 $x_i-x_j$ 的最大值) 注意这里只考虑 $\texttt{dis0[A][i],dis1[j][B]}$ 是不行的,因为一边算出的信息不一定全。 ```c++ fo(i,1,n) if(i!=A&&i!=B) fo(j,i+1,n) if(j!=A&&j!=B) { if( dis0[A][i]>dis1[j][B] || dis0[A][j]>dis1[i][B] ) cnt1++; else if( (dis0[A][i]==dis1[j][B]&&dis1[A][i]==dis0[j][B]) || (dis0[A][j]==dis1[i][B]&&dis1[A][j]==dis0[i][B]) ) cnt2++; else if( dis1[A][i]<dis0[j][B] || dis1[A][j]<dis0[i][B] ) cnt3++; } ``` [Code](https://www.luogu.com.cn/record/59494073) 总结:用最短路径求差分方程组的最大解,用最长路径求差分方程组的最小解。 ### Trick 4 - 用 Tarjan 代替 Bellman-Ford/SPFA 这个东西真的很难想到( **例题:**[P3275 [SCOI2011]糖果](https://www.luogu.com.cn/problem/P3275) 一眼差分约束,然后发现 $n$ 是 $10^5$ 级别的,很慌,可能会卡 SPFA。 注意到这里的差值**只有 $1$**,因此可以考虑先用 Tarjan 缩点然后在 DAG 上 DP。 具体看代码。[Code](https://www.luogu.com.cn/record/59507124) ### Trick 5 - 取 $\log$ 来化乘为加 - [P4926 [1007]倍杀测量者](https://www.luogu.com.cn/problem/P4926) 显然先上一个二分答案,二分完自然想到从反面(没有任何人女装)的情况入手。 列出不等式组跑最短路,若无解则说明有人女装,否则无人女装。 但是,列出的不等式是这样的: $$ d_u\geq kd_v $$ 这个不等式**含有倍数**。 怎么处理?我们发现**可以通过 $\log$ 使运算降级**,因此: $$ \ln d_u\geq\ln d_v+\ln k $$ 这样就可以了。[Code](https://www.luogu.com.cn/record/59610624) --- 由于做法实在多变,就不放了qwq