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");
}
}
上一篇: 快手“拒绝折叠”背后