差分约束学习指南

前置芝士

求解差分约束系统,有 m条约束条件,每条都为形如 \(( x_a-x_b\geq c_k)\)\((x_a-x_b\leq c_k)\)\(x_a=x_b\) 的形式,判断该差分约束系统有没有解。

题意 转化 连边
\((x_a - x_b \geq c)\) \((x_b - x_a \leq -c)\) add(a, b, -c);
\(( "x_a - x_b \leq c")\) \((x_a - x_b \leq c)\) add(b, a, c);
\((x_a = x_b)\) \((x_a - x_b \leq 0, \space x_b - x_a \leq 0)\) add(b, a, 0), add(a, b, 0);

差分约束

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

\[\begin{cases} x_{c_1}-x_{c'_1}\leq y_1 \\x_{c_2}-x_{c'_2} \leq y_2 \\ \cdots\\ x_{c_m} - x_{c'_m}\leq y_m\end{cases} \]

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

[input]

第一行为两个正整数 \(n,m\),代表未知数的数量和不等式的数量。

接下来 \(m\) 行,每行包含三个整数 \(c,c',y\),代表一个不等式 \(x_c-x_{c'}\leq y\)

[output]

一行,\(n\) 个数,表示 \(x_1 , x_2 \cdots x_n\) 的一组可行解,如果有多组解,请输出任意一组,无解请输出 NO

[a.in]

3 3
1 2 3
2 3 -2
1 3 1

[a.out]

5 3 5

\(\begin{cases}x_1-x_2\leq 3 \\ x_2 - x_3 \leq -2 \\ x_1 - x_3 \leq 1 \end{cases}\)

一种可行的方法是 \(x_1 = 5, x_2 = 3, x_3 = 5\)

\(\begin{cases}5-3 = 2\leq 3 \\ 3 - 5 = -2 \leq -2 \\ 5 - 5 = 0\leq 1 \end{cases}\)

[datas]

\(1\leq n,m \leq 5\times 10^3\)\(-10^4\leq y\leq 10^4\)\(1\leq c,c'\leq n\)\(c \neq c'\)

solved

我们观察下这些不等式,像在最短路里的三角不等式,于是,发现了这些性质:

我们设 dis 数组代表长度,跟最短路里的dis 概念一样。

\[dis_v\leq dis_u+w(u\to v)\Longrightarrow dis_v-dis_u\leq w(u\to v) \]

所以,我们只要对于每个不等式 \(x_i−x_j≤y\),连一条从j 到i 长度为y 的边,跑一遍最短路,即可得到一组解。备注:我们设 cnt 数组来表示这个点记录了几次,如果这个图有负环,说明 \(cnt_i≥n\),则要输出无解。这就是差分约束算法的重要思想。

[寻找所有x<=0最大解]

因此对于这类不等式组的求解,我们可以将其抽象成一个有 n 个点的最短路问题,对于不等式\(x_i−x_j≤y\),建一条从j连向i 边权为y的单向边。

补充: 如果存在符号相反的不等式,如\(x_i−x_j≥y\),我们可以通过给两边乘−1 的方式使其变号,变为 \(x_j−x_i≤−y\)

建完图后,为了每个点的可达性,我们要新建一个超级源点s=n+1 向每个点连出一条边权为 0的边。

const int N=5010;
int n,m,h[N],idx;
struct edge{
	int v,ne,w;
}e[N<<1];
int dis[N],cnt[N];
bool vis[N];
queue<int> q;
void add(int u,int v,int w){
	e[++idx].v=v;
	e[idx].ne=h[u];
	e[idx].w=w;
	h[u]=idx;
}
bool spfa(int s){
	dis[s]=0;
	vis[s]=1;
	q.push(s);
	while(!q.empty()){
		int u=q.front();q.pop();
		vis[u]=0;
		for(int i=h[u];i;i=e[i].ne){
			int v=e[i].v;
			if(dis[v]>dis[u]+e[i].w){
				dis[v]=dis[u]+e[i].w;
				if(vis[v]==0){
					cnt[v]++;
					if(cnt[v]>=n+1) return false;//多加了一个超级源点,所以总共的点数是n+1,最多也就被每个点更新一次
					vis[v]=1;
					q.push(v);
				}

			}
		}
	}
	return true;
}
void solve(){
	cin>>n>>m;
	int s=n+1;
	for(int i=1;i<=m;i++){
		int u,v,w;
		cin>>u>>v>>w;
		add(v,u,w);
	}
	for(int i=1;i<=n;i++){
		add(s,i,0);
	}
	memset(dis,0x3f,sizeof(dis));
	if(!spfa(s)){
		cout<<"NO"<<endl;
		return;
	}
	for(int i=1;i<=n;i++)
			cout<<dis[i]<<" ";
	cout<<endl;
}

[寻找所有x>=0的最小值]

const int N=5010;
struct edge{
	int ne,v,w;
}e[N<<1];
int idx,h[N];
int cnt[N],dis[N];
bool vis[N];
queue<int> q;
int n,m;
void add(int u,int v,int w){
	e[++idx]={h[u],v,w};
	h[u]=idx;
}
bool spfa(int s){
	q.push(s);
	vis[s]=true;
	// cnt[s]++;
	dis[s]=0;
	while(!q.empty()){
		int u=q.front();q.pop();
		vis[u]=false;
		for(int i=h[u];i;i=e[i].ne){
			int v=e[i].v;
			int w=e[i].w;
			if(dis[v]<dis[u]+w){
				dis[v]=dis[u]+w;
				if(!vis[v]){
					q.push(v);
					vis[v]=true;
				if(++cnt[v]>=n+1) {return false;}
				}
			}
		}
	}
	return true;

}
void solve(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int u,v,w;
		cin>>u>>v>>w;
		add(u,v,-w);
	}
	int s=n+1;
	for(int i=1;i<=n;i++) add(s,i,0);
	memset(dis,-1,sizeof(dis));
	if(!spfa(s)){
		cout<<"NO"<<endl;
	}else{
		for(int i=1;i<=n;i++) cout<<dis[i]<<" ";
		cout<<endl;
	}
}

糖果

幼儿园里有 \(N\) 个小朋友,\(\text{lxhgww}\) 老师现在想要给这些小朋友们分配糖果,要求每个小朋友都要分到糖果。但是小朋友们也有嫉妒心,总是会提出一些要求,比如小明不希望小红分到的糖果比他的多,于是在分配糖果的时候,\(\text{lxhgww}\) 需要满足小朋友们的 \(K\) 个要求。幼儿园的糖果总是有限的,\(\text{lxhgww}\) 想知道他至少需要准备多少个糖果,才能使得每个小朋友都能够分到糖果,并且满足小朋友们所有的要求。

[input]

输入的第一行是两个整数 \(N\)\(K\)。接下来 \(K\) 行,表示这些点需要满足的关系,每行 \(3\) 个数字,\(X\)\(A\)\(B\)

  • 如果 \(X=1\), 表示第 \(A\) 个小朋友分到的糖果必须和第 \(B\) 个小朋友分到的糖果一样多;
  • 如果 \(X=2\), 表示第 \(A\) 个小朋友分到的糖果必须少于第 \(B\) 个小朋友分到的糖果;
  • 如果 \(X=3\), 表示第 \(A\) 个小朋友分到的糖果必须不少于第 \(B\) 个小朋友分到的糖果;
  • 如果 \(X=4\), 表示第 \(A\) 个小朋友分到的糖果必须多于第 \(B\) 个小朋友分到的糖果;
  • 如果 \(X=5\), 表示第 \(A\) 个小朋友分到的糖果必须不多于第 \(B\) 个小朋友分到的糖果;

[output]

输出一行,表示 \(\text{lxhgww}\) 老师至少需要准备的糖果数,如果不能满足小朋友们的所有要求,就输出 \(-1\)

[a.in]

5 7
1 1 2
2 3 2
4 4 1
3 4 5
5 4 5
2 3 5
4 5 1

[a.out]

11

[datas]

对于所有的数据,保证 \(K\leq100000, 1\leq X\leq5, 1\leq A, B\leq N\)

solved

我们将小朋友当做点,小朋友之间限制条件当做边。

对于X=1、X=3、X=5时,我们不难知道,想要使用最少的糖果数量,那么最好的方式就是两个人糖果数量一样。

对于X=2,那么A=B-1最优。

对于X=4,那么B=A-1最优。

(1)建图

比如A要少于B,则建一条A->B的边,这样拓扑排序下来,可以保证处理每个点的糖果数量时,可以从小处理到大,符合上面的要求。我们就需要额外记录边权,X=1、3、5边权为0,X=2、4边权为1。X=1时需要建双向边!即A->B且B->A。

(2)判断无解

在建新图用来拓扑排序时,看两个点是否在同一个环内,在且边权为1则无解。

(3)更新糖果数量

dp[当前更新的点] = max(dp[当前更新的点],dp[当前删除的点] + nnei[当前删除的点][第j个邻居].边权)

(4)总体思路:建图(X=1:建边权为0的双向边,X=2、X=4建边权为1的单向边,X=3、X=5时建边权为0的单向边)——>Tarjan——>边建新图边判断无解——>用新图拓扑排序,并更新每个点所需要的糖果数量。

// A要少于B,则建一条A->B的边
const int N = 100010;
int n, k;

/*
scc:每个节点的归属新编号
cnt:缩点个数
low:最小可到达时间戳
dfn:每个节点的深度优先搜索时间戳
idx:时间
*/
int scc[N], cnt, low[N], dfn[N], idx, tot[N];
bool ins[N];
stack<int> s;
vector<node> e[N];
vector<node> newe[N];
struct node {
	int ne;
	int v;
};

int dp[N];
int in[N];
ll ans;

void tarjan(int u) {
	low[u] = dfn[u] = ++idx;
	ins[u] = true;
	s.push(u);
	int len = e[u].size();
	for (int i = 0; i < len; i++) {
		int v = e[u][i].ne;
		if (dfn[v] == 0) {
			tarjan(v);
			low[u] = min(low[v], low[u]);
		} else {
			if (ins[v])
				low[u] = min(low[u], low[v]);
				//low[u]=min(low[u],dfn[v]);
		}
	}
	if (dfn[u] == low[u]) {
		cnt++;
		scc[u] = cnt;
		ins[u] = false;
		tot[cnt]++;
		while (s.top() != u) {
			int t = s.top();
			ins[t] = false;
			scc[t] = cnt;
			s.pop();
			tot[cnt]++;
		}
		s.pop();
	}
}
void solve() {
	cin >> n >> k;
	for (int i = 1; i <= k; i++) {
		int t, x, y;
		cin >> t >> x >> y;
		switch (t) {
		case 1: {
			e[x].push_back((node) {y, 0});
			e[y].push_back((node) {x, 0});
			break;
		}
		case 2: {
			e[x].push_back((node) {y, 1});
			break;
		}
		case 3: {
			e[y].push_back((node) {x, 0});
			break;
		}
		case 4: {
			e[y].push_back((node) {x, 1});
			break;
		}
		case 5: {
			e[x].push_back((node){y, 0});
			break;
		}
		}
	}
		for(int i=1;i<=n;i++){
			if(dfn[i]==0) tarjan(i);
		}
		for(int i=1;i<=n;i++){
			int len=e[i].size();
			for(int j=0;j<len;j++){
				int v=e[i][j].ne;
				int xx=scc[i];
				int yy=scc[v];
				if(xx==yy&&e[i][j].v==1){
					cout<<-1<<endl;
					return;
				}
				if(xx!=yy){
					newe[xx].push_back((node){yy,e[i][j].v});
					in[yy]++;
					//会有重边
				}
			}
		}
		queue<int> q;
		for(int i=1;i<=cnt;i++){
			if(!in[i]){
				q.push(i);
				dp[i]=1;
			}
			while(!q.empty()){
				int cur=q.front();
				q.pop();
				int len=newe[cur].size();
				for(int i=0;i<len;i++){
					int v=newe[cur][i].ne;
					in[v]--;
					dp[v]=max(dp[v],dp[cur]+newe[cur][i].v);
					if(!in[v]) q.push(v);
				}
			}
		}
		for(int i=1;i<=cnt;i++){
			ans+=(ll)dp[i]*tot[i];
		}
		cout<<ans<<endl;
	}
posted @ 2023-11-22 13:25  White_Sheep  阅读(2)  评论(0编辑  收藏  举报