什么是差分约束系统?

差分约束可以理解为满足n个条件的不等式。通常属于

为什么能用spfa?

1.每个不等式能转化为一条有向边,比如X\: j-X\: i>=n可以转化为从节点i到j有一条权为n的边

2.求解差分约束系统等价于寻找一组变量值 x_1, x_2, ..., x_n,满足所有边对应的约束。这与最短路松弛操作的逻辑完全一致:

最短路中,dist[j] ≤ dist[i] + c(从 i 到 j 的最短路径不超过 i 的距离加边权 c

差分约束中,x_j ≤ x_i + c(变量 x_j 不超过 x_i 加常数 c

———————————————————————————————————————————

如何根据不等式建边?

对于任意不等式 x_j - x_i ≤ c(这是最基础的形式):

1.等价变形为 x_j ≤ x_i + c

2.对应图中一条从节点 i 指向节点 j 的有向边,边的权重为 c

为什么如此?

因为spfa的松弛操作对应“如果dist[i]>d[x]+w[i]就更新”,这个不等式相当于x_j ≤ x_i + c

所有建边规则的本质,都是把差分约束的不等式,统一转化为最短路算法能理解的 “dist[j] ≤ dist[i] + c” 形式。只有这样,才能通过 SPFA 等最短路算法,既找到满足所有约束的x(即dist数组),又能检测负环(判断无解)。

——————————————————————————————————————————

为什么负环无解?

在差分约束系统中,负环的存在会导致无限满足 “变量差值小于某个负数” 的矛盾循环,使得变量取值不存在合法解。​

具体来说,差分约束将每个不等式 x_j - x_i ≤ c 转化为图中从 i 到 j、边权为 c 的有向边,求解等价于找一组变量值满足所有边的约束。若图中存在负环(环上所有边权之和为负数),则沿着环循环遍历会得到矛盾:​

  • 例如负环 a→b→c→a,边权分别为 -1、-1、-1,对应约束 x_b - x_a ≤ -1、x_c - x_b ≤ -1、x_a - x_c ≤ -1。​
  • 将三个不等式相加,左边抵消为 0,右边总和为 -3,得到 0 ≤ -3,这是恒不成立的矛盾。​
  • 同时,绕负环循环次数越多,变量差值会无限减小(如 x_b 会比 x_a 无限小),无法找到固定的合法数值,因此系统无解。

——————————————————————————————————————————

如何spfa判负环?

判断任意一个节点入队次数是否大于总节点数。因为存在负环会让spfa判断两点间权时每经过一次就让总和越变越小,于是重复更新,重复入队

来看代码

bool spfa(int s1) {
    for(int i = 1; i <= n+1; i++) {
        dis[i] = 2147483647;
        cnt[i] = 0;
        inq[i] = false;
    }
    dis[s1] = 0;
    q.push(s1);
    inq[s1] = true;
    cnt[s1]++;
    
    int x;
    while(!q.empty()) {
        x = q.front();
        q.pop();
        inq[x] = false;
        
        for(int i = head[x]; i; i = ne[i]) {
            if(dis[to[i]] > dis[x] + w[i]) {
                dis[to[i]] = dis[x] + w[i];
                
                if(!inq[to[i]]) {
                    q.push(to[i]);
                    inq[to[i]] = true;
                    cnt[to[i]]++;
                    if(cnt[to[i]] > n) {
                        return true;
                    }
                }
            }
        }
    }
    return false;
}

很常规,如果你不知道基础的spfa,可以看一下前几期的博客最短路spfa和多层图(P1073 [NOIP 2009 提高组] 最优贸易)题解-CSDN博客

———————————————————————————————————————————

很好,既然如此,我们来写一道模板

P5960 【模板】差分约束 - 洛谷

先来看一下如何建边,我们得到的等式是差小于一个常数。根据我们上面梳理的内容,我们要先转化为一个数小于等于一个和。也就是x_c1≤x_c1'+y_1

令x_c1=a,x_c1'=b,y1=c,所以我们对于第一个等式,建边就是add(b,a,c)

同时,我们要建立一个“超级节点”,确保这个节点和整个图上的所有节点都有边相连,因为图不一定是连通图,spfa有可能扫不到有些点

差不多这样,AC代码
 

#include<bits/stdc++.h>
using namespace std;

int read(){
    int s=0,fl=1;char w=getchar();
    while(w>'9'||w<'0'){if(w=='-')fl=-1;w=getchar();}
    while(w<='9'&&w>='0'){s=s*10+(w^48);w=getchar();}
    return fl*s;
}
void out(int x){
    if(x<0)putchar('-'),x=-x;
    if(x<10)putchar(x+'0');
    else out(x/10),putchar(x%10+'0');
}
int n,m;
bool inq[100010];
int head[100010],to[100010],ne[100010],w[100010],tot;
int dis[100010],cnt[100010];
queue<int>q;
void add(int x,int y,int v){
	to[++tot]=y;
	ne[tot]=head[x];
	head[x]=tot;
	w[tot]=v;
}
bool spfa(int s1) {
    for(int i = 1; i <= n+1; i++) {
        dis[i] = 2147483647;
        cnt[i] = 0;
        inq[i] = false;
    }
    dis[s1] = 0;
    q.push(s1);
    inq[s1] = true;
    cnt[s1]++;
    
    int x;
    while(!q.empty()) {
        x = q.front();
        q.pop();
        inq[x] = false;
        
        for(int i = head[x]; i; i = ne[i]) {
            if(dis[to[i]] > dis[x] + w[i]) {
                dis[to[i]] = dis[x] + w[i];
                
                if(!inq[to[i]]) {
                    q.push(to[i]);
                    inq[to[i]] = true;
                    cnt[to[i]]++;
                    if(cnt[to[i]] > n) {
                        return true;
                    }
                }
            }
        }
    }
    return false;
}

int main(){
	n=read();m=read();
	for(int i=1;i<=m;i++){
		int a,b,c;
		a=read();b=read();c=read();
		add(b,a,c);
	}
	for(int i=1;i<=n;i++){
		add(n+1,i,0);
	}
	if(spfa(n+1)){
		cout<<"NO";
		return 0;
	}
	for (int i = 1; i <= n; i++){
		printf("%d ", dis[i]);
	}
		
    return 0;
}

双倍经验

P1993 小 K 的农场 - 洛谷

国际惯例先骗分,全输出No有85分

来正经做

根据题目,我们能列出三个不等式

还有一个就是相等,移项后可得一个减去另一个=0;也就是这条边的边权为0

其实就很简单了,建图要满足第一个等式,我们只需要两边都乘上-1就能改变不等号的方向。

直接上代码吧,其他都是一样的

#include<bits/stdc++.h>
using namespace std;

int read(){
    int s=0,fl=1;char w=getchar();
    while(w>'9'||w<'0'){if(w=='-')fl=-1;w=getchar();}
    while(w<='9'&&w>='0'){s=s*10+(w^48);w=getchar();}
    return fl*s;
}
void out(int x){
    if(x<0)putchar('-'),x=-x;
    if(x<10)putchar(x+'0');
    else out(x/10),putchar(x%10+'0');
}
int n,m,a,b,c,tot;
int head[1000010],ne[1000010],to[1000010],w[1000010];
int d[1000010],cnt[1000010];
bool inq[1000010];
queue<int>q;
void add(int x,int y,int c){
	to[++tot]=y;
	ne[tot]=head[x];
	head[x]=tot;
	w[tot]=c;
}
bool spfa(int s){
	for(int i=1;i<=n+1;i++){
		d[i]=1e18;
		inq[i]=false;
		cnt[i]=0;
	}
	d[s]=0;
	q.push(s);
	inq[s]=true;
	cnt[s]++;
	int x;
	while(!q.empty()){
		x=q.front();
		q.pop();
		inq[x]=false;
		for(int i=head[x];i;i=ne[i]){
			if(d[to[i]]>w[i]+d[x]){
				d[to[i]]=w[i]+d[x];
				if(!inq[to[i]]){
					q.push(to[i]);
					inq[to[i]]=true;
					cnt[to[i]]++;
					if(cnt[to[i]]>n){
						return true;
					}
				}
			}
		}
	}
	return false;
}
int main(){
	n=read();
	m=read();
	for(int i=1;i<=m;i++){
		int op=read();
		if(op==1){
			a=read();b=read();c=read();
			add(a,b,-c);
		}
		else if(op==2){
			a=read();b=read();c=read();
			add(b,a,c);
		}
		else{
			a=read();b=read();
			add(a,b,0);
			add(b,a,0);
		}
	}
	for(int i=1;i<=n;i++){
		add(n+1,i,0);
	}
	if(spfa(n+1)){
		cout<<"No";
		return 0;
	}
	cout<<"Yes";
    return 0;
}

Logo

惟楚有才,于斯为盛。欢迎来到长沙!!! 茶颜悦色、臭豆腐、CSDN和你一个都不能少~

更多推荐