差分约束
差分约束
前言
又是 20231012联考 T4 考到。。。
于是不会,前面的题也没有补,开始学习!
定义
差分约束是什么,看起来和图论没有一点关系。。。
差分约束系统是一种特殊的 \(n\) 元一次不等式组,\(n\) 个变量 \(x_1,x_2,\dots ,x_n\),和 \(m\) 个约束条件,
每个条件都是形如 \(x_i-x_j \le c_k\),让你求一组解。
发现把这个式子变形一下就是 \(x_i \le x_j +c_k\),
这个东西和单源最短路中的 \(dis[v] \le dis[u]+w\) 非常相似。
于是我们考虑把 \(x_i\) 看作一个节点,从 \(x_j\) 到 \(x_i\) 建一条长度为 \(c_k\) 的有向边。
好妙哦
过程
建出图之后,我们再建立一个源点 \(dis[0]=0\),
向每一个点链接一条长度为 \(0\) 的边,跑单元最短路,
如果图中存在负环,于是差分约束无解,
否则,\(x_i=dis[i]\) 就是一组解。
例题
先来考虑如何判负环——
P3385 【模板】负环
负环就是在用 spfa 跑的过程中,如果入队次数超过了 \(n\) ,那就一定存在负环。
时间复杂度有可能被卡到 \(O(nm)\)。
#include <bits/stdc++.h>
using namespace std;
const int N=1e4+5;
int T,n,m,head[N],tot=0,cnt[N],dis[N];
struct edge{
int v,nxt,w;
}e[N<<1];
bool vis[N];
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
void add(int u,int v,int w){
e[++tot]=(edge){v,head[u],w};
head[u]=tot;
}
void spfa(){
memset(dis,0x3f,sizeof(dis));
memset(vis,false,sizeof(vis));
dis[1]=0;vis[1]=true;
queue<int> q;q.push(1);
while(!q.empty()){
int u=q.front();q.pop();
vis[u]=false;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].v,w=e[i].w;
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
if(!vis[v]){
if(++cnt[v]>=n){puts("YES");return;}
q.push(v);vis[v]=true;
}
}
}
}
puts("NO");
}
int main(){
/*2023.10.13 H_W_Y P3385 【模板】负环 spfa*/
T=read();
while(T--){
n=read();m=read();
memset(head,0,sizeof(head));tot=0;
memset(cnt,0,sizeof(cnt));
for(int i=1,u,v,w;i<=m;i++){
u=read();v=read();w=read();
add(u,v,w);
if(w>=0) add(v,u,w);
}
spfa();
}
return 0;
}
P5590 赛车游戏
有些时候一定不要忘记打标记!
你发现边权的范围是 \([1,9]\) ,那么在有解的情况下,
对于有向边 \((u,v)\),需要满足:
于是我们把式子转化一下,就变成了:
我们就可以用差分约束做了。
具体就是先用 dfs 找出在 \(1 \sim n\) 路径上面的边,(dfs 需要简单优化,就是走过不再走)
建出一个新图,再跑 spfa 判负环即可。
最后输出就是通过判断 \(d[v]-d[u]\) 的大小来确定边权。
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,head[N],tot=0,dis[N],m,cnt[N],h[N],t=0;
struct edge{
int v,nxt,w;
}e[N<<1],E[N<<1];
struct node{
int u,v;
}a[N<<1];
bool vis[N],pp[N];
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
void print(int x,char s){
int p[15],tmp=0;
if(x==0) putchar('0');
if(x<0) putchar('-'),x=-x;
while(x){
p[++tmp]=x%10;
x/=10;
}
for(int i=tmp;i>=1;i--) putchar(p[i]+'0');
putchar(s);
}
void add(int u,int v,int w){
e[++tot]=(edge){v,head[u],w};
head[u]=tot;
}
void add2(int u,int v){
E[++t]=(edge){v,h[u]};
h[u]=t;
}
bool spfa(){
memset(dis,0x3f,sizeof(dis));
memset(vis,false,sizeof(vis));
dis[1]=0;vis[1]=true;
queue<int>q;q.push(1);
while(!q.empty()){
int u=q.front();q.pop();
vis[u]=false;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].v,w=e[i].w;
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
if(!vis[v]){
if(++cnt[v]>n) return true;
q.push(v),vis[v]=true;
}
}
}
}
return false;
}
bool dfs(int u){
if(u==n||pp[u]) return true;
for(int i=h[u];i;i=E[i].nxt){
int v=E[i].v;
if(!vis[i]){
vis[i]=true;
if(dfs(v)){
pp[u]=true;
add(u,v,9);add(v,u,-1);
}
}
}
return pp[u];
}
int main(){
/*2023.10.13 H_W_Y P5590 赛车游戏 差分约束*/
n=read();m=read();
for(int i=1,u,v;i<=m;i++) u=read(),v=read(),add2(u,v),a[i]=(node){u,v};
if(!dfs(1)||spfa()){puts("-1");return 0;}
print(n,' ');print(m,'\n');
for(int i=1;i<=m;i++){
int ans=dis[a[i].v]-dis[a[i].u];
print(a[i].u,' ');print(a[i].v,' ');
if(ans>=1&&ans<=9) print(ans,'\n');
else puts("1");
}
return 0;
}
ARC177F-Gateau
现在回到那联考题。
我们通过前缀和+二分分析了以下内容。
现在我们考虑一个答案 \(x\) ,我们想验证它是否合法。
由于题目中是求每 \(n\) 个数的和,
所以我们设 \(s_i\) 为前缀和数组,且序列编号从 \(0\) 开始,于是 \(s_0=0\),则:
于是发现其实 \(s_{i+n}-s_i\) 和 \(s_i-s_{i-n}\) 是一个东西,
所以就有了一个对于 \(s_{i+n}-s_i\) 的上界和下界:
现在考虑如何构造,
由于 \(s_i \ge s_{i-1}\) ,所以我们可以依次枚举过来,进行贪心:
- 若 \(l_i \le s_{i+n-1}-s_{i-1} \le r_i\) ,说明前面已经满足条件,则 \(s_i=s_{i-1},s_{i+n}=s_{i+n-1}\)。
- 若 \(s_{i+n-1}-s_{i-1}\lt l_i\),则 \(s_i=s_{i-1},s_{i+n}=s_{i-1}+l_i\),这样可以保证 \(s_{i+n}\gt s_{i+n-1}\)
- 若 \(s_{i+n-1}-s_{i-1}\gt r_i\),则 \(s_i=s_{i+n-1}-r_i,s_{i+n}=s_{i+n-1}\)。
发现这样枚举只用算前面 \(n\) 个,那怎么知道是满足条件呢?
很容易想到,最后的 \(s_i\) 需要满足:
你发现 \(s_n-s_{n-1}\) 和 \(s_{2n-1}\) 一定是随 \(x\) 增加而增加,
因为在 \(x\) 增加的过程中,相当于每次把上限扩大了。
于是就可以用二分解决。
于是就得到了一个双重二分的方法,外层枚举 \(x\) ,内层二分 \(s_0\) 和 \(s_n\) 的大小即可。
时间复杂度 \(O(n \log^2 v)\)
现在发现 \(l_i \le s_{i+n}-s_i \le r_i\) 这个式子非常像差分约束,
于是我们就可以用差分约束来做一做。
下标从 \(1\) 开始,注意每次在最后还要保证:
#include <bits/stdc++.h>
using namespace std;
const int N=3e5+5;
int n,c[N],head[N],tot=0,dis[N],cnt[N],l,r,dep[N];
bool vis[N];
struct edge{
int v,nxt,w;
}e[N<<1];
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
void add(int u,int v,int w){
e[++tot]=(edge){v,head[u],w};
head[u]=tot;
}
bool spfa(){
memset(vis,false,sizeof(vis));
memset(dis,0x3f,sizeof(dis));
memset(dep,0,sizeof(dep));
dis[0]=0;vis[0]=true;
queue<int> q;q.push(0);
while(!q.empty()){
int u=q.front();q.pop();
vis[u]=false;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].v,w=e[i].w;
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
dep[v]=dep[u]+1;
if(dep[v]>(n<<1)) return false;
if(!vis[v]){
q.push(v);vis[v]=true;
if(dis[q.back()]<dis[q.front()]) swap(q.front(),q.back());
}
}
}
}
return true;
}
bool chk(int x){
int L,R;
for(int i=0;i<=(n<<1);i++) head[i]=cnt[i]=0;tot=0;
for(int i=1;i<=n;i++){
L=c[i];R=x-c[i+n];
if(L>R) return false;
add(i,i+n,R);add(i+n,i,-L);
}
for(int i=1;i<=(n<<1);i++) add(i,i-1,0);
add(0,(n<<1),x);add((n<<1),0,-x);
return spfa();
}
int main(){
n=read();
for(int i=1;i<=(n<<1);i++) c[i]=read(),r=max(r,c[i]);
l=0;r<<=1;
while(l<r){
int mid=(l+r)/2;
if(chk(mid)) r=mid;
else l=mid+1;
}
printf("%d\n",l);
return 0;
}
现在考虑如何优化?
发现我们判负环跑 \(spfa\) 的时间太长了,
于是我们考虑高级的操作——人脑判负环!
把这张图画出来,发现特别特殊,
现在我们考虑把这个图进行分层,于是就变成了这样:
发现只有两条红色的边会构成环,
于是负环上面要么包含 \(0\) ,要么包含 \(s_{n+1} \to s_{n}\),
于是我们把红边删掉,
从 \(0\) 开始跑最短路,再从 \(n\) 开始跑最短路,判断最后的距离是否非负即可。
如果最短路是负数——那么一定存在负环。
用 \(dp\) 可以做到线性。
时间复杂度 \(O(n \log v)\),别忘了开 long long!!!
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=5e5+5;
const ll inf=1e18;
int n,c[N],l,r;
ll dis[N];
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
void wrk(int x){
for(int i=2*n-1;i>n;i--){
dis[i]=min(dis[i+1],dis[i-n+1]+x-c[i+1]);
dis[i-n]=min(dis[i-n+1],dis[i]-c[i-n+1]);
}
}
void init(){for(int i=0;i<=(n<<1);i++) dis[i]=inf;}
bool chk(int x){
init();dis[0]=0;dis[(n<<1)]=x;wrk(x);//从 0 出发跑
if(dis[1]<0||dis[n+1]-c[1]<0||dis[(n<<1)]-c[1]-c[n+1]<0) return false;
init();dis[n]=0;wrk(x);//从 n 出发,先把前面的点更新了
dis[0]=min(dis[0],dis[1]);dis[(n<<1)]=min(dis[(n<<1)],dis[0]+x);//处理 n~2n 里面的初值
wrk(x);//更新 n ~ 2n 从 n 出发的值
if(dis[0]+x-c[n+1]<0||dis[n+1]<0||dis[(n<<1)]-c[n+1]<0) return false;
init();dis[(n<<1)]=0;wrk(x);//从 2n 开始跑
dis[0]=min(dis[0],dis[1]);
if(dis[0]+x-c[(n<<1)+1]+x-c[n+1]<0||dis[n+1]+x-c[(n<<1)+1]<0) return false;
return true;
}
int main(){
n=read();
for(int i=1;i<=(n<<1);i++) c[i]=read(),r=max(r,c[i]);
for(int i=1;i<=n;i++) l=max(l,c[i]+c[i+n]);
c[(n<<1)+1]=c[1];r<<=1;
while(l<r){
int mid=(l+r)/2;
if(chk(mid)) r=mid;
else l=mid+1;
}
printf("%d\n",l);
return 0;
}
Conclusion
差分约束时间复杂度过大时可以考虑人脑判负环。