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

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

这个问题大概就是:

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

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

 

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就一定被选上,这种情况无解,例如:

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

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

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

例如:

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

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

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

 

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

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

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

对于关系

(中间那个是与符号,后面的1表示真)

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

(离散数学有讲)

同时又有其逆否命题:

所以对于这个关系式则需要连接 非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为假 ,即:

其他建边方式类似

#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算法求完之后就是一个逆向的拓扑序,因为这个算法是递归处理的,较深的节点处理完毕,编完号之后才轮到浅的节点,所以是逆序的拓扑序

还是看图:

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

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

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

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

 

 

 

例题 

poj3678  Katu Puzzle

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

题意:

这个连边就有讲究了:

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

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

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

这个时候就只能选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 的形式

假设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");
	}
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值