差分约束
知识介绍
差分约束:
差分约束有两种形式:
形式1,给定若干个不等式,类似Xi-Xj<=Ci,判断所有不等式是否有解,有解给出变量的一组解。其中Xi、Xj均为变量,Ci均为常量。
形式2,给定若干个不等式,类似Xi-Xj>=Ci,判断所有不等式是否有解,有解给出变量的一组解其中Xi、Xj均为变量,Ci均为常量。
实际案例理解:
假设组织去春游,有几个同学出门时间要互相配合:
小明出发得比小红早,最多早 10 分钟:t红-t明<=10
小刚出发得比小红晚,最多晚 5 分钟:t刚-t红<=5
小芳和小刚几乎同时出发,最多相差 2 分钟:t芳-t刚<=2
这些条件就组成了一个差分约束系统,而问题就是1所有同学能不能同时出发(满足这些条件),以及如果能,他们大概什么时候出发?
将每个“人”的出发时间看作一个点,每个约束 “Xi-Xj<=Ci” 就像画一条有方向的路,从Xj指向Xi,路上写着一个数字 Ci,表示:“从 Xj 走到 Xi ,最多可以花 Ci 分钟”。
这就是差分约束的解决方法,即转化成一个图论问题来解决:
1. 把不等式变成图的一条边
○ 比如条件 $x_i - x_j <= c$,我们可以把它变形为$x_i <= x_j + c$ 。
即:最短路时的松弛操作【动态转移】?$dist[i] <= dist[j] + weight(j, i)$
○ 所以,我们可以从节点 j 向节点 i 连一条长度为 c 的有向边。
2. 建立一个超级源点
○ 为了能从一个点走到所有点,我们通常会建立一个“超级源点” 0,并从它向所有其他点连一条长度为 0 的边。
3. 跑最短路算法(通常是SPFA)
○ 如果我们把不等式都转化为 $x_i - x_j <= c$的形式并建图,那么跑完最短路后,每个点的$dist[i]$ 就是变量 $x_i$的一个可行解。
○ 注意:如果图中有负权环,说明这些不等式矛盾,无解。
核心要点:
● 求最小值,跑最长路 (形如xi - xj >= c , 求下界)
● 求最大值,跑最短路 (形如xi - xj <= c , 求上界)
● 遇到 ==,就拆成 >= 和 <= 两个条件。
● 遇到 > 或 <,可以通过 +1 或 -1 来变成 >= 或 <=。
代码实现
Bellman-Ford算法是基于松弛操作的单源最短路算法,可以解决有负权边但不能有负环(保证最短路存在)的图。以下是一些它的相关知识介绍。
负环
定义:在一个带权有向图中,如果存在一个环(从一个顶点出发,经过若干条边后又能回到该顶点),并且这个环上所有边的权重之和为负数,那么这个环就被称为负环。
联系差分约束来理解,可以假设有三个人 A、B、C,他们回家时间有约束:
- B 比 A 晚 ≤ 5 分钟:Tb-Ta<=5
- C 比 B 晚 ≤ 5 分钟:Tc-Tb<=5
- A 比 C 晚 ≤ -20 分钟(也就是 A 必须比 C 早至少 20 分钟):Ta-Tc<=-20→Tc-Ta<=20
画出对应图

看环。从 A 到 B 最多 5 分钟,B 到 C 最多 5 分钟,C 到 A 最多 -20 分钟。把它们加起来:5 + 5 + (-20) = -10。
这说明:“A 到 A 的时间 ≤ -10 分钟”,也就是说自己比自己还要早 10 分钟,这是矛盾的。
正环同理,即为在一个带权有向图中,如果存在一个环,并且这个环上所有边的权重之和为正数。(虽然这个算法是找负环的,但是也提一下)
可以想象,如果存在负环,那么在寻找最短路的时候,这个环里的数据会循环着一直变小,最终变为无穷小,负环也就不存在了。正环相较于最大路来说同理。
松弛操作
假设源点为A,从A到任意点F的最短距离为distance [F]
假设从点P出发某条边,去往点S,边权为W如果发现,distance [P]+W<distance [S],也就是通过该边可以让distance [S]变小
那么就说,P出发的这条边对点S进行了松弛操作
时间复杂度
假设点的数量为N,边的数量为M,每一轮时间复杂度0(M)。最短路存在的情况下,因为1次松弛操作会使1个点的最短路的边数+1而从源点出发到任何点的最短路最多走过全部的n个点,所以松弛的轮数必然<=n-1
所以Bellman -ford算法时间复杂度0(M*N)
实现过程
1.初始化,d[s]=0,d[其它点]=+∞;
2.执行多轮循环。每一轮考察每条边每条边都尝试进行松弛操作,那么若干点的distance 会变小
3.当某一轮循环中没有成功的松弛操作时,算法停止。
判断负环
已知如果从A出发存在最短路(没有负环),那么松弛的轮数必然<=n-1而如果从A点出发到达一个负环,那么松弛操作显然会无休止地进行下去。所以,如果发现从A点出发,在第n轮时松弛操作依然存在,说明从A点出发能够到达一个负环
代码:
struct edge{int v,w;};//出边,权值
vector<edge> e[5005];
int dis[5005],n,m;//dis为距离
void add(int a,int b,int c){
e[b].push_back({a,c});
}
bool spfa(int s){
memset(dis,inf,sizeof dis);//初始化为较大值
dis[s]=0;//点记为0
bool f;//是否松驰
for(int i=1; i<=n; i++){//n轮操作
f=0;//更新为不松弛
for(int u=1; u<=n; u++){//遍历所有点
if(dis[u]==inf) continue;//跳过源点目前还无法到达的点,防止没有意义的计算
for(auto j : e[u]){//遍历该点邻接点
int v=j.v, w=j.w;
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
f=1;//松弛该点
}
}
}
if(!f) break;//该轮没有松弛操作,则所有点已松弛完毕,退出循环
}
return f;//若n轮遍历完仍存在可松弛的点,即f=ture,则存在负环
}
NBellman-Ford + SPFA优化
可以发现,每一轮考察所有的边看看能否做松弛操作是不必要的,因为只有上一次被某条边松弛过的节点,其所连接的边才有可能引起下一次的松弛操作。所以用队列来维护“这一轮哪些节点的distance 变小了”,下一轮只需要对这些点的所有边,考察有没有松弛操作即可。
实现过程:
1.初始化,s入队,标记s在队内,d[s]=0,d[其它点]=正无穷
2.从队头弹出u点,标记u不在队内;
3.枚举u的所有出边,执行松弛操作。记录从s走到v的边数,并判负环。如果v不在队内则把v压入队尾,并打上标记;4.重复2,3步操作,直到队列为空。
代码
struct edge{ int v,w;;
vector< edge> e[n];int d[ N] , cnt [ N] , vis[N]:
queue <int>q;//队列
bool spfa( int s)i
memset( d, inf, sizeof d)
d[ s] = 0; vis[s]=1;q.push(s);
while(q.size()i
int u= q. front( : q. pop( : vis [ u]=0;
for(auto ed :e[u]){if(d[v]>d[u]+w)i d[v]=d[u]+w;
cnt[v]=cnt[u]+1;//记录边数if( cnt[ v] > = n ) return true //v点被更新且不在队内,则入队
int v= ed. v, W= ed. W:
if( ! vis[ v] ) q. push( v) , vis[v]=1;
}
return false
题目练习
1. P5960 【模板】差分约束算法
○ 链接: https://www.luogu.com.cn/problem/P5960
○ 题目大意: 纯模板题。给你一系列 x_i - x_j <= c 的约束,判断是否有解,并输出一组解。
○ 考察点: 最基础的建图,SPFA判负环,以及模板代码的书写。
○ 小提示: 一定要建立超级源点!
#include<bits/stdc++.h>
using namespace std;
const int inf=0x7fffffff;
struct edge{int v,w;};//出边,权值
vector<edge> e[5005];
int dis[5005],n,m;
void add(int a,int b,int c){
e[b].push_back({a,c});
}
bool spfa(int s){
memset(dis,inf,sizeof dis);
dis[s]=0;
bool f;
for(int i=1; i<=n; i++){
f=0;
for(int u=1; u<=n; u++){
if(dis[u]==inf) continue;
for(auto j : e[u]){
int v=j.v, w=j.w;
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
f=1;
}
}
}
if(!f) break;
}
return f;
}
int main(){
cin>>n>>m;
for(int i=1; i<=m; i++){
int c,c1,y;//xc-xc1<=y➡xc<=xc1+y
cin>>c>>c1>>y;
add(c,c1,y);
}
for(int i=1; i<=n; i++){
add(n+1,i,0); add(i,n+1,0);
}
if(!spfa(n+1)){
for(int i=1; i<=n; i++) cout<<dis[i]<<" ";
}
else cout<<"NO";
return 0;
}
2. P1250 种树
○ 链接: https://www.luogu.com.cn/problem/P1250
○ 题目大意: 区间 [B, E] 内至少种 T 棵树。求最少种多少棵树。
○ 考察点: 这是差分约束的“经典应用题”。如何把实际问题转化为不等式:
- 设 S[i] 为前 i 个位置种树的总数。
- 区间 [B, E] 至少种 T 棵 -> S[E] - S[B-1] >= T
- 隐含条件:S[i] - S[i-1] >= 0 (每个位置种树数非负) 和 S[i] - S[i-1] <= 1 (每个位置最多种一棵)。
○ 注意: 求“最少种多少”,是求最小值,所以我们要找最长路。需要把不等式都变成 >= 的形式。
#include<bits/stdc++.h>
using namespace std;
const int INF=-1e9;
const int M=30010;
struct edge{int v, w;};
vector<edge> e[M];
int dis[M];//距离
bool vis[M];//是否在队列
int n,m;
void add(int a,int b,int c) {
e[a].push_back({b,c});
}
void spfa(int s) {
for (int i=0; i<=n; i++) {//初始化
dis[i]=INF;
vis[i]=0;
}
dis[s]=0;//源点更新为0
queue<int> q;
q.push(s);//源点入队
vis[s]=1;
while (!q.empty()) {
int u=q.front();
q.pop();
vis[u]=0;
for (auto ed : e[u]) {//遍历u邻接点
int v=ed.v, w=ed.w;
if (dis[v]<dis[u]+w){
dis[v]=dis[u]+w;
if (!vis[v]){//邻接点符合约束条件
q.push(v);//临界点入队
vis[v]=1;
}
}
}
}
}
int main() {
cin>>n>>m;
for (int i=1; i<=n; i++) {
add(i-1,i,0);
add(i,i-1,-1);
}
for (int i=1; i<=m; i++) {
int b,e1,t;
cin>>b>>e1>>t;
add(b-1,e1,t);
}
spfa(0);
cout<<dis[n];
return 0;
}
部分内容摘自
https://www.yuque.com/wuxinlin-j5mkh/acm/usqg7fwgnnuefkbz
(点击链接即可查看更详细的内容👍👍非常好的讲解与总结,使我大脑旋转)

浙公网安备 33010602011771号