欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

2-sat 学习日记 + 例题(洛谷p4782 + poj3678 + poj3683)

程序员文章站 2022-06-02 17:45:17
...

sat就是satisfiability的缩写,2-sat也叫做2-可满足性 问题

这个问题大概就是:

有N个变量,每个变量只有两种可能的取值,给M个条件,每个条件是对这些取值的限制,求是否存在对N个变量的合法赋值使得M个条件均得到满足

具体看一道模板题: https://www.luogu.org/problem/P4782

2-sat 学习日记 + 例题(洛谷p4782 + poj3678 + poj3683)

 

tarjan可以协助解决这类问题,当然假如最后需要输出字典序,那么只能用dfs暴力求解,这里先说tarjan,并且假如不需要输出字典序的话一般也使用tarjan,dfs留个坑

 

用图的方式解决2-sat的思想大概是这样:

N种变量,每个变量只有两个可能的取值,那么就可以用两个点分别表示该变量的两种取值,我习惯点i表示i的赋值为真,点i+N表示i的赋值为假。

有M个对取值的限制  可以转化为  图的边。一般来说,对于一条从a到b的有向边我们赋予它意义:假如选择点a,那么一定要选择点b。

对于建好边的图,每个i和i+N必须选择其中一个,并且只能选择其中一个。

考虑图在什么情况下无解,那么就是对于某一个i和i+N,选不到它们的任何一个或者这两个都必须被选上的时候,无解。

而我们处理图的方式使得我们一定选得到点,为什么了解算法流程就知道了(先放下)

那么就剩下一种情况,就是假如存在某个点i,假如i被选上,那么i+N就一定被选上,这种情况无解,例如:

2-sat 学习日记 + 例题(洛谷p4782 + poj3678 + poj3683)

此时点1和点3要么都不选,要么必须都被选上,图的意义就是点1同时赋值为真和假,此时无解。

考虑更一般的情况,当点i和点i+N处于同一个强连通分量的时候,无解,因此此时强连通分量的任何一个点被选中,其余的点都会被选中。

而当图中的任何一点i和i+N都不处于同一个强连通分量的时候,此时有解。

例如:

2-sat 学习日记 + 例题(洛谷p4782 + poj3678 + poj3683)

虽然说假如选了1,那么3就会被选,但是我可以不选1,只选4 和 3,此时是有解的

假如i和i+N不在同一个强连通分量,那么就有一种选法使得i和i+N不同时被选(选择不同的强连通分量)

一般可以使用tarjan求有向图的强连通分量。

 

下一个很关键的问题就是:如何建边。

关于如何建边我也不是特别清楚,只能说一下我的理解。

上述图的关系我们使用有向边来表示,这是个有向图。

对于关系

2-sat 学习日记 + 例题(洛谷p4782 + poj3678 + poj3683)(中间那个是与符号,后面的1表示真)

通过逻辑变化,可以变化为:

2-sat 学习日记 + 例题(洛谷p4782 + poj3678 + poj3683)(离散数学有讲)

同时又有其逆否命题:

2-sat 学习日记 + 例题(洛谷p4782 + poj3678 + poj3683)

所以对于这个关系式则需要连接 非A-->B  和 非B-->A

关于建边,还有一点是要有“唯一性”,也就是对于一条A->B 的边,表示的是选了A一定要选B,而不是选了A可以选B

比如 A | B == 1,这个关系就不能连接A->B   A->非B , 因为当A为真的时候,可以选择B,也可以选择非B,不满足唯一性,此时应该连接的是非A->B , A为假那么一定会选择B

最后一点就是,使用tarjan算法只有当i和i+N处于强连通分量的时候才会认为图中非法,对于其他没有连接边的点来说,这些点都是默认可选。当条件中有哪个点一定不能被选的时候,需要我们构造边使得这个点选了就非法,这个在poj 3678 有用到(下面有提到)

 

最后一个问题就是解决输出方案的问题,结合模板题再说

 

模板题:洛谷P4782

https://www.luogu.org/problem/P4782

 

假如 x1为真 或 x2为假 ,即:
2-sat 学习日记 + 例题(洛谷p4782 + poj3678 + poj3683)

其他建边方式类似

#include<bits/stdc++.h>
using namespace std;
const int maxn = 2e6 + 7;
int n, m;
int Head[maxn], Nxt[maxn], To[maxn];
int dfn[maxn], low[maxn], id, belong[maxn], blo_id;
int tot, st[maxn], st_ind, ins[maxn];
void add_edge(int fro, int to) {
	Nxt[++tot] = Head[fro];
	To[tot] = to;
	Head[fro] = tot;
}
void tarjan(int x) {
	dfn[x] = low[x] = ++id;
	st[++st_ind] = x;
	ins[x] = 1;
	for (int i = Head[x]; i; i = Nxt[i]) {
		const int &to = To[i];
		if (!dfn[to]) {
			tarjan(to);
			low[x] = min(low[x], low[to]);
		}
		else if (ins[to]) low[x] = min(low[x], dfn[to]);
	}
	if (low[x] == dfn[x]) {
		int z;
		++blo_id;
		do {
			z = st[st_ind--];
			belong[z] = blo_id;
			ins[z] = 0;
		} while (z != x);
	}
}
int main() {
	cin >> n >> m;
	int fro, to, opt1, opt2;
	for (int i = 1; i <= m; i++) {
		scanf("%d %d %d %d", &fro, &opt1, &to, &opt2);
		if (opt1 == 0) fro += n;
		if (opt2 == 0) to += n;
		add_edge((fro > n ? fro - n : fro + n), to);
		add_edge((to > n ? to - n : to + n), fro);
	}
	for (int i = 1; i <= 2 * n; i++) if (!dfn[i]) tarjan(i);
	for (int i = 1; i <= n; i++)
		if (belong[i] == belong[i + n]) {
			printf("IMPOSSIBLE");
			return 0;
		}
	printf("POSSIBLE\n");
	for (int i = 1; i <= n; i++) {//输出方案
		printf("%d ", (belong[i] < belong[i + n]));

	}
}

关于如何输出方案的问题:

对于点i和i+N,假如它们都不属于同一个强连通分量,那么应该选择它们中的哪一个输出呢?

因为tarjan算法求完之后就是一个逆向的拓扑序,因为这个算法是递归处理的,较深的节点处理完毕,编完号之后才轮到浅的节点,所以是逆序的拓扑序

还是看图:

2-sat 学习日记 + 例题(洛谷p4782 + poj3678 + poj3683)

此时的标号是它们的拓扑序号

选择拓扑序号较小的(表示该需要更深),发生冲突的概率(i和i+N一起被选)就越小。

像这个图,假如选择序号为4的,那么它后面所有序号都需要被选择,包括序号为1那个点

而假如选择序号为1的,它后面的节点就比序号为4的更少(因为它更深),需要被选择的序号就更少,发生冲突的几率就更少。实际上对于i 和 i+N,总是选择它们中间更深的那个就不会发生冲突(因为i只能和i+N发生冲突)

 

 

 

例题 

poj3678  Katu Puzzle

http://poj.org/problem?id=3678

题意:

2-sat 学习日记 + 例题(洛谷p4782 + poj3678 + poj3683)

这个连边就有讲究了:

1.假如A AND B 为真,那么A为真B一定也要为真,但是这个时候不能连边A-->B ,因为:

2-sat 学习日记 + 例题(洛谷p4782 + poj3678 + poj3683)

假如只连接A->B,这种情况下选择非A 和 非B也是合法的,对于某个点不能被选,需要我们自己构造边使得这个点选了就错误。

因此这种情况需要连接非A->A   非B->B

2-sat 学习日记 + 例题(洛谷p4782 + poj3678 + poj3683)

这个时候就只能选A 和 B 了,这才是 A AND B == 1  的正确连边方法

2.假如A AND B == 0 ,此时应该连接 A --> 非B     B--> 非A      而不能连接 非A --> B   和 非A --> B,这个就是唯一性的问题,假如A 是假,那么此时B可以为真也可以为假,这样子不行,我们连边需要满足唯一性。

其他的就类似了,这里就略过了。

代码:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 2e6 + 7;
int Head[maxn], To[maxn<<1], Nxt[maxn<<1];
int dfn[maxn], low[maxn], belong[maxn], blo_id;
int ins[maxn], st[maxn], st_ind;
int tot, n, m, id;
void add_edge(int fro, int to) {
	Nxt[++tot] = Head[fro];
	To[tot] = to;
	Head[fro] = tot;
}
void tarjan(int now) {
	low[now] = dfn[now] = ++id;
	ins[now] = 1;
	st[++st_ind] = now;
	for (int i = Head[now]; i; i = Nxt[i]) {
		const int &to = To[i];
		if (!dfn[to]) {
			tarjan(to);
			low[now] = min(low[now], low[to]);
		}
		else if (ins[to])
			low[now] = min(low[now], dfn[to]);
	}
	if (dfn[now] == low[now]) {
		++blo_id;
		int z;
		do {
			z = st[st_ind--];
			ins[z] = 0;
			belong[z] = blo_id;
		} while (z != now);
	}
}
int main() {
	int n, m;
	cin >> n >> m;
	int in1, in2, res;
	char s[5];
	for (int i = 1; i <= m; i++) {
		scanf("%d %d %d", &in1, &in2, &res);
		scanf("%s", s);
		if (s[0] == 'A') {  // AND
			if (res == 1) add_edge(in1 + n, in1), add_edge(in2 + n, in2);
			else add_edge(in1, in2+n), add_edge(in2, in1+n);
		}
		else if (s[0] == 'O') { //OR
			if (res == 1) add_edge(in1 + n, in2), add_edge(in2 + n, in1);
			else add_edge(in1, in1 + n), add_edge(in2, in2 + n);
		}
		else { //XOR
			if (res == 1) {
				add_edge(in1, in2 + n);
				add_edge(in1 + n, in2);
				add_edge(in2, in1 + n);
				add_edge(in2 + n, in1);
			}
			else {
				add_edge(in1 + n, in2 + n);
				add_edge(in1, in2);
				add_edge(in2 + n, in1 + n);
				add_edge(in2, in1);
			}
		}
	}
	for (int i = 0; i < 2 * n; i++)
		if (!dfn[i])
			tarjan(i);
	for (int i = 0; i < n; i++) 
		if (belong[i] == belong[i + n]) {
			puts("NO");
			return 0;
		}
	puts("YES");
	return 0;
}

 

 

例题 

poj 3683 Priest John's Busiest Day

http://poj.org/problem?id=3683

题意:

2-sat 学习日记 + 例题(洛谷p4782 + poj3678 + poj3683)

 

显然对于每对情侣来说都有两个时间段可行,并且需要选出其中一段,这个符合2 - sat 的形式

假设Ai 表示第i对情侣开始的时间段,Bi表示第i对情侣的结束的时间段,Ai --> Bj 表示第i对情侣的开始时间段和第j对情侣的结束时间段不冲突。

同样是出于唯一性考虑,假如第i对情侣和第j对情侣的时间段都没有冲突,那么这两对情侣就不适合连边,因为此时Ai 既可以连Aj 也可以连 Bj

只有当第i对情侣和第j对情侣的某一个时间段有冲突的时候,才可以连边,具体看代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<vector>
#include<queue>
#include<map>
using namespace std;
const int maxn = 1e5 + 7;
int Head[maxn], Nxt[maxn<<2], To[maxn<<2];
int dfn[maxn], low[maxn], id;
int st[maxn], st_ind, blo_id, belong[maxn];
int d[maxn], bg1[maxn], ed1[maxn], bg2[maxn], ed2[maxn];
int n, tot, ins[maxn];
void add_edge(int fro, int to) {
	Nxt[++tot] = Head[fro];
	To[tot] = to;
	Head[fro] = tot;
}
void tarjan(int now) {
	dfn[now] = low[now] = ++id;
	st[++st_ind] = now, ins[now] = 1;
	for (int i = Head[now]; i; i = Nxt[i]) {
		const int &to = To[i];
		if (!dfn[to]) {
			tarjan(to);
			low[now] = min(low[now], low[to]);
		}
		else if (ins[to])
			low[now] = min(low[now], dfn[to]);
	}
	if (dfn[now] == low[now]) {
		int z;
		++blo_id;
		do {
			z = st[st_ind--];
			ins[z] = 0;
			belong[z] = blo_id;
		} while (z != now);
	}
}
int main() {
	cin >> n;
	int in1_h, in1_m, in2_h, in2_m;
	for (int i = 1; i <= n; i++) {
		scanf("%d:%d %d:%d %d", &in1_h, &in1_m, &in2_h, &in2_m, d + i);
		bg1[i] = in1_h * 60 + in1_m; //全部转化为分钟
		bg2[i] = in2_h * 60 + in2_m - d[i];
		ed1[i] = bg1[i] + d[i];
		ed2[i] = bg2[i] + d[i];
	}
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n; j++) {
			if (i == j) continue;
			//只有当存在时间冲突的时候,建边才具有唯一性
			if (!(ed1[i] <= bg1[j] || bg1[i] >= ed1[j]))
				add_edge(i, j + n);
			if (!(ed1[i] <= bg2[j] || bg1[i] >= ed2[j]))
				add_edge(i, j);
			if (!(ed2[i] <= bg1[j] || bg2[i] >= ed1[j]))
				add_edge(i + n, j + n);
			if (!(ed2[i] <= bg2[j] || bg2[i] >= ed2[j]))
				add_edge(i + n, j);
		}
	}
	for (int i = 1; i <= 2*n; i++)
		if (!dfn[i])
			tarjan(i);
	for (int i = 1; i <= n; i++)
		if (belong[i] == belong[i + n]) {
			printf("NO\n");
			return 0;
		}
	puts("YES");
	for (int i = 1; i <= n; i++) {
		int bg, ed;
		if (belong[i] < belong[i + n]) {
			bg = bg1[i];
			ed = ed1[i];
		}
		else {
			bg = bg2[i];
			ed = ed2[i];
		}
		if (bg / 60 < 10) printf("0%d:", bg / 60);
		else printf("%d:", bg / 60);
		if (bg % 60 < 10) printf("0%d ", bg % 60);
		else printf("%d ", bg % 60);
		bg = ed;
		if (bg / 60 < 10) printf("0%d:", bg / 60);
		else printf("%d:", bg / 60);
		if (bg % 60 < 10) printf("0%d", bg % 60);
		else printf("%d", bg % 60);
		printf("\n");
	}
}

 

相关标签: acm 2-sat tarjan