SDSC整理(Day2 差分约束)

差分约束算法

差分约束算法是一种可以帮助你解数学题的算法

\(lead\)

给你一组包含 \(m\) 个不等式,有 \(n\) 个未知数的形如

\(\begin{cases}x_{c1}-x_{c1\prime}≤y_1\\x_{c2}-x_{c2\prime}≤y_2\\......\\x_{cm}-x_{cm\prime}≤y_m\end{cases}\)

的不等式组,求任意一组这个不等式组的解。

。。。。。。

没学差分约束的我:数学题!!!

不,这是一道图论题。

话说dp也可以转图论,万物皆可图论?

\(pre-knowledge\)

\(SPFA\)它死了,但是没完全死

\(description\)

差分约束系统是一种特殊的 \(n\) 元一次不等式组,

它包含 \(n\) 个变量个 \(m\) 个约束条件,

每个约束条件都是两个变量做差形成的,

形如 \(x_i-x_j≤ c_k\)\(c_k\) 是一个常数,

差分约束算法的实现就是求出一组解,使其满足所有的约束条件。

我们来看其中的一个约束条件:

\[x_i-x_j≤c_k \]

我们对这个式子进行变形,得到

\[x_i≤x_j+c_k \]

看着上面这个式子,你是否感到十分地熟悉???

\[dis[v]≤dis[u]+w[u-v] \]

这不就是我们的最短路吗!!!

所以,我们就把求解不等式的问题转换成了最短路的问题,

上面的 \(i\) 就对应着 \(v\) ,上面的 \(j\) 就对应着 \(u\) ,上面的 \(c_k\) 就对应着 \(w[u-v]\) ,我们就可以建一条从 \(j\)\(i\) 的边权为 \(c_k\) 的路径,然后跑最短路,每一个 \(dis[i]\) 的值就对应这相应的 \(x_i\) 的值。

我们考虑无解的情况,如果存在负环,则最短路无解,该不等式组无解。

因为负环会卡掉 \(dijkstra\) ,所以我们要用 \(SPFA\) 跑最短路,关于 SPFA,它又活了,统计一下每一个点入队的次数,如果超过所有点的数量,证明出现了负环。

不会负环判断的可以先做这道题 P3385 【模板】负环

好的,现在我们就可以轻松的切掉模板题。

P5960 【模板】差分约束算法

code

/*
	差分约束
	date:2022.7.26
	worked by respect_lowsmile 
*/
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N=1e5+5;
const int INF=0x3f3f3f3f;
struct node
{
	int to,next,w;
};
node edge[N<<1];
int num,flag,n,m;
int head[N],dis[N],vis[N],cnt[N];
void add(int u,int v,int w)
{
	num++;
	edge[num].to=v;
	edge[num].w=w;
	edge[num].next=head[u];
	head[u]=num;
}
void SPFA()
{
	queue<int> q;
	for(int i=1;i<=n;++i)
		dis[i]=INF;
	memset(cnt,0,sizeof(cnt));
	memset(vis,0,sizeof(vis));
	dis[0]=0,vis[0]=1;
	q.push(0);
	while(!q.empty())
	{
		int now=q.front();
		q.pop();
		vis[now]=0;
		for(int i=head[now];i;i=edge[i].next)
		{
			int v=edge[i].to,w=edge[i].w;
			if(dis[now]+w<dis[v])
			{
				dis[v]=dis[now]+w;
				if(!vis[v])
				{
					vis[v]=1,cnt[v]++;
					if(cnt[v]==n+1)
					{
						flag=1;
						return ;
					}
					q.push(v);
				}
			}
		}
	}
	return ;
}
int main()
{
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;++i)  add(0,i,0);
	for(int i=1;i<=m;++i)
	{
		int u,v,w;
		scanf("%d %d %d",&u,&v,&w);
		add(v,u,w);
	}
	SPFA();
	if(flag==1)  printf("NO");
	else
	{
		for(int i=1;i<=n;++i)
		  printf("%d ",dis[i]);
	}
	return 0;
}

关于最短路的两种跑法和 \(dis\) 的初值问题

这也是我在学差分约束时最不理解的地方,希望对大家有帮助。

跑法问题

上面程序运行的结果是 \(0\)\(-2\)\(0\) ,考虑为什么。

你会发现我加了一句 for(int i=1;i<=n;++i) add(0,i,0);

在最短路中这是什么意思???

\(x_i≤x_0+0\)\(x_0\) 也就是 \(dis[0]=0\) ,也就相当于 \(x_i≤ 0\)

能明白为什么出现这样的结果了吧。

可以发现我们相当于建了一个超级源点 \(0\) ,然后跑的最短路,那为什么要建这个超级源点呢???因为图不一定联通啊,如果不建超级源点而从 \(1\) 开始,那么就会有不与 \(1\) 联通的点更新不到,结果就会出错。

当然,不建超级源点也是可以的,我们可以在初始化的时候把所有的点全部都入队,这样就可以保证更新到所有的点。

\(dis\) 的初值问题

此外,如果你是建了超级源点跑的 \(SPFA\) ,那么 \(dis[]\) 的初值就对于结果没有任何的贡献,只是为了在跑最短路的时候方便更新路径,

但是如果使用不建超级源点的跑法,那么 \(dis[]\) 的值就相当于你跑最短路的基础,也就是相当于最短路结果的上界和最长路结果的下界,

所以如果你不建超级源点, \(dis[]\) 的初值设为 \(100000\) ,那么你会发现你的模板题跑出的结果是 \(0\)\(99998\)\(100000\)

最短路和最长路问题

最长路求的是最小值

最长路的模型:

\[dis[v]≥dis[u]+w[u-v] \]

所以我们要把式子都转化为 \(x_i≥x_j+c_k\) 的形式,然后连一条 \(j\)\(i\) 的权值为 \(c_k\) 的边。

无解的情况就是有正环。

最短路求的是最大值

最短路在上面都说了,这里不在赘述。

一些式子的转换(最长路为例)

\(a=b\) \(\Rightarrow\) \(a \ge b-0\) && \(b\ge a-0\)

\(a<b\) \(\Rightarrow\) \(b \ge a+1\)

\(a \ge b\) \(\Rightarrow\) \(a \ge b+0\)

\(a>b\) \(\Rightarrow\) \(a \ge b+1\)

\(a \le b\) \(\Rightarrow\) \(b \ge a+0\)

差分约束的一些应用

本板块可能会持续更新......

前缀和的差分约束

简单题:P1250 种树

综合题:P2294 [HNOI2005]狡猾的商人

我们以简单题为例

很明显,要求最少的树的数目个数,所以是跑最长路。

我们用前缀和维护区间的树的个数,很明显, \(bi\)\(ci\) 之间的树的个数就是 \(sum[c_i]-sum[b_i-1]\)

我们可以很明显地找到约束条件:

\[sum[c_i]-sum[b_i-1] \ge t_i \]

\[sum[i]-sum[i-1] \ge 0 \]

\[sum[i]-sum[i-1] \le 1 \]

第一个条件是因为两个点之间的树的数量不小于 \(t_i\) ,第二和第三个条件是因为每个单位最多只能种一棵树。

我们用 \(dis[]\) 维护 \(sum[]\) ,那么我们的结果就是 \(sum[n]\) ,也就是 \(SPFA\) 跑出来的 \(dis[n]\)

code

/*
	差分约束
	date:2022.7.27
	worked by respect_lowsmile 
*/
#include<iostream>
#include<queue>
using namespace std;
const int N=1e5+5;
const int INF=0x3f3f3f3f;
struct node
{
	int to,next,w;
};
node edge[N<<2];
int num,n,h;
int head[N],dis[N],vis[N];
void add(int u,int v,int w)
{
	num++;
	edge[num].to=v;
	edge[num].w=w;
	edge[num].next=head[u];
	head[u]=num;
}
void SPFA()
{
	queue<int> q;
	for(int i=1;i<=n;++i)
		dis[i]=-INF;
	dis[0]=0,vis[0]=1;
	q.push(0);
	while(!q.empty())
	{
		int now=q.front();
		q.pop();
		vis[now]=0;
		for(int i=head[now];i;i=edge[i].next)
		{
			int v=edge[i].to,w=edge[i].w;
			if(dis[v]<dis[now]+w)
			{
				dis[v]=dis[now]+w;
				if(!vis[v])
				{
					vis[v]=1;
					q.push(v);
				}
			}
		}
	}
}
int main()
{
	scanf("%d %d",&n,&h);
	for(int i=1;i<=h;++i)
	{
		int u,v,w;
		scanf("%d %d %d",&u,&v,&w);
		add(u-1,v,w);
	}
	for(int i=1;i<=n;++i)
	{
		if(i)  add(i-1,i,0);
		if(i!=n)  add(i,i-1,-1);
	}
	SPFA();
	printf("%d",dis[n]);
	return 0;
}

综合题的解法和简单题是一样的,我们假设综合题大家也会了

\(end\)

差分约束其实最重要的就是寻找题目中的不等关系,然后构造不等式求解,要注意题目中的隐含条件。

continue.....

posted @ 2022-08-09 09:06  respect_lowsmile  阅读(35)  评论(0编辑  收藏  举报