既然选择了远方,便只顾风雨兼行|

H_W_Y

园龄:1年11个月粉丝:28关注:15

2023-10-14 13:31阅读: 30评论: 0推荐: 0

差分约束

差分约束

前言

又是 20231012联考 T4 考到。。。

于是不会,前面的题也没有补,开始学习!

定义

差分约束是什么,看起来和图论没有一点关系。。。

差分约束系统是一种特殊的 n 元一次不等式组,n 个变量 x1,x2,,xn,和 m 个约束条件,

每个条件都是形如 xixjck,让你求一组解。

发现把这个式子变形一下就是 xixj+ck

这个东西和单源最短路中的 dis[v]dis[u]+w 非常相似。

于是我们考虑把 xi 看作一个节点,从 xjxi 建一条长度为 ck 的有向边。

好妙哦

过程

建出图之后,我们再建立一个源点 dis[0]=0

向每一个点链接一条长度为 0 的边,跑单元最短路,

如果图中存在负环,于是差分约束无解,

否则,xi=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),需要满足:

1d[v]d[u]9

于是我们把式子转化一下,就变成了:

d[v]d[u]+9d[u]d[v]+(1)

我们就可以用差分约束做了。

具体就是先用 dfs 找出在 1n 路径上面的边,(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 个数的和,

所以我们设 si 为前缀和数组,且序列编号从 0 开始,于是 s0=0,则:

0i<nsi+nsiaini<2nsisinxai

于是发现其实 si+nsisisin 是一个东西,

所以就有了一个对于 si+nsi 的上界和下界:

li=ai,ri=xai+nlisi+nsiri

现在考虑如何构造,

由于 sisi1 ,所以我们可以依次枚举过来,进行贪心:

  1. lisi+n1si1ri ,说明前面已经满足条件,则 si=si1,si+n=si+n1
  2. si+n1si1<li,则 si=si1,si+n=si1+li,这样可以保证 si+n>si+n1
  3. si+n1si1>ri,则 si=si+n1ri,si+n=si+n1

发现这样枚举只用算前面 n 个,那怎么知道是满足条件呢?

很容易想到,最后的 si 需要满足:

sn1sn,s2n1x

你发现 snsn1s2n1 一定是随 x 增加而增加,

因为在 x 增加的过程中,相当于每次把上限扩大了。

于是就可以用二分解决。

于是就得到了一个双重二分的方法,外层枚举 x ,内层二分 s0sn 的大小即可。

时间复杂度 O(nlog2v)


现在发现 lisi+nsiri 这个式子非常像差分约束,

于是我们就可以用差分约束来做一做。

下标从 1 开始,注意每次在最后还要保证:

xs2ns0x

#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 的时间太长了,

于是我们考虑高级的操作——人脑判负环!

把这张图画出来,发现特别特殊,

现在我们考虑把这个图进行分层,于是就变成了这样:

image

发现只有两条红色的边会构成环,

于是负环上面要么包含 0 ,要么包含 sn+1sn

于是我们把红边删掉,

0 开始跑最短路,再从 n 开始跑最短路,判断最后的距离是否非负即可。

如果最短路是负数——那么一定存在负环。

dp 可以做到线性。

时间复杂度 O(nlogv),别忘了开 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

差分约束时间复杂度过大时可以考虑人脑判负环

本文作者:H_W_Y

本文链接:https://www.cnblogs.com/H-W-Y/p/chafenyueshu.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   H_W_Y  阅读(30)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起