【Coel.学习笔记】2-SAT 问题
终于结束网络流了,真有够累的……
SAT 问题相关概念
SAT 是 的英文缩写,意为“适应性”。对于若干个命题,每个命题都有且只有“真”和“假”两种取值。接下来会给出若干个条件,每个条件都形如“ 为真/假或 为真/假或……”的形式,需要找到一个命题取值,使得所有条件都可以得到满足。
如果一个条件关联了 个命题,就称为 K-SAT 问题。特殊地,每个条件只关联 个命题就叫 2-SAT 问题。
对于 的情况, SAT 问题是 完全的。但当 时,我们就可以以线性时间复杂度解决问题。
求解算法
在学习算法之前,我们先要了解一点数理逻辑的知识。
假设一个命题 是命题 的充分条件,即 ,那么就会有 为真时 一定为真, 为假时 可能为假。对于式子 ,如果上面的情况正确就为真,反之为假。
为了更形象地了解这个“推导”的含义,下面给出了 不同的取值时 的值。
写 Markdown 的时候上面一坨的 $ 看着好吓人
从上表可以发现, ,且 。
我们可以把这个问题转化为图论问题。把所有命题放在一张有向图上,那么有向图的路径就代表了一个推导关系。接下来只要求解一个强连通分量就好了。
参考代码如下:
// Problem: 2-SAT 问题
// Contest: AcWing
// URL: https://www.acwing.com/problem/content/2404/
// Memory Limit: 512 MB
// Time Limit: 5000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int maxn = 4e6 + 10;
int n, m;
int head[maxn], nxt[maxn], to[maxn], cnt;
int dfn[maxn], low[maxn], stk[maxn], top;
int bel[maxn], vis[maxn], idx, tot;
void add(int u, int v) { nxt[cnt] = head[u], to[cnt] = v, head[u] = cnt++; }
void tarjan(int u) {
dfn[u] = low[u] = ++tot;
stk[++top] = u, vis[u] = true;
for (int i = head[u]; ~i; i = nxt[i]) {
int v = to[i];
if (!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
} else if (vis[v])
low[u] = min(low[u], dfn[v]);
}
if (low[u] == dfn[u]) {
int v;
idx++;
do {
v = stk[top--];
vis[v] = false;
bel[v] = idx;
} while (v != u);
}
}
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
memset(head, -1, sizeof(head));
for (int i = 1; i <= m; i++) {
int x, a, y, b;
cin >> x >> a >> y >> b;
x--, y--;
add(2 * x + !a, 2 * y + b);
add(2 * y + !b, 2 * x + a);
}
for (int i = 0; i < n * 2; i++)
if (!dfn[i]) tarjan(i);
for (int i = 0; i < n; i++)
if (bel[i * 2] == bel[i * 2 + 1]) cout << "IMPOSSIBLE", exit(0);
cout << "POSSIBLE" << '\n';
for (int i = 0; i < n; i++)
if (bel[i * 2] < bel[i * 2 + 1])
cout << '0' << ' ';
else
cout << '1' << ' ';
return 0;
}
例题讲解
[POJ3283] Priest John's Busiest Day
有 对情侣在这天准备结婚,每对情侣都预先计划好了婚礼举办的时间,其中第 对情侣的婚礼从时刻 开始,到时刻 结束。婚礼有一个必须的仪式:站在牧师面前聆听上帝的祝福。这个仪式要么在婚礼开始时举行,要么在结束时举行。
第 对情侣需要 分钟完成这个仪式,即必须选择 或 两个时间段之一。
牧师约翰想知道他能否满足每场婚礼的要求,即给每对情侣安排 或 ,使得这些仪式的时间段不重叠。
若能满足,还需要帮牧师求出任意一种具体方案。
解析:把每一场婚礼看作一个命题,那么每个命题就有两个取值:在开头做祝福和在结束做祝福。 枚举一下每个时间段的冲突关系,就可以得到命题的取值。这样就转化成 2-SAT 问题了。
bool check(int a, int b, int c, int d) { return d > a && b > c; }
int main(void) {
cin >> n;
memset(head, -1, sizeof(head));
for (int i = 0; i < n; i++) {
int s0, s1, t0, t1, d;
scanf("%d:%d %d:%d %d", &s0, &s1, &t0, &t1, &d);
a[i] = {s0 * 60 + s1, t0 * 60 + t1, d};
}
for (int i = 0; i < n; i++)
for (int j = 0; j < i; j++) {
auto x = a[i], y = a[j];
if (check(x.l, x.l + x.t, y.l, y.l + y.t))
add(i, j + n), add(j, i + n);
if (check(x.l, x.l + x.t, y.r - y.t, y.r))
add(i, j), add(j + n, i + n);
if (check(x.r - x.t, x.r, y.l, y.l + y.t))
add(i + n, j + n), add(j, i);
if (check(x.r - x.t, x.r, y.r - y.t, y.r))
add(i + n, j), add(j + n, i);
}
for (int i = 0; i < n * 2; i++)
if (!dfn[i]) tarjan(i);
for (int i = 0; i < n; i++)
if (bel[i] == bel[i + n]) cout << "NO", exit(0);
cout << "YES" << '\n';
for (int i = 0; i < n; i++) {
auto x = a[i];
int l = x.l, r = x.r, t = x.t;
if (bel[i] < bel[i + n])
printf("%02d:%02d %02d:%02d\n", l / 60, l % 60, (l + t) / 60,
(l + t) % 60);
else
printf("%02d:%02d %02d:%02d\n", (r - t) / 60, (r - t) % 60, r / 60,
r % 60);
}
return 0;
}
[NOI2017] 游戏
洛谷传送门
题面有点长,用一个引用放着。
小 L 计划进行 场游戏,每场游戏使用一张地图,小 L 会选择一辆车在该地图上完成游戏。
小 L 的赛车有三辆,分别用大写字母 、、 表示。地图一共有四种,分别用小写字母 、、、 表示。
其中,赛车 不适合在地图 上使用,赛车 不适合在地图 上使用,赛车 不适合在地图 上使用,而地图 则适合所有赛车参加。
适合所有赛车参加的地图并不多见,最多只会有 张。
场游戏的地图可以用一个小写字母组成的字符串描述。例如: 表示小L计划进行 场游戏,其中第 场和第 场的地图类型是 ,适合所有赛车,第 场和第 场的地图是 ,不适合赛车 ,第 场和第 场的地图是 ,不适合赛车 ,第 场和第 场的地图是 ,不适合赛车 。
小 L 对游戏有一些特殊的要求,这些要求可以用四元组 来描述,表示若在第 场使用型号为 的车子,则第 场游戏要使用型号为 的车子。
你能帮小 L 选择每场游戏使用的赛车吗?如果有多种方案,输出任意一种方案。如果无解,输出-1
。
解析: NOI 的题面和码量都比较大,要认真读题找到内在性质。
如果没有 地图,那么这道题就是一个很简单的 2-SAT 问题,因为每张地图只有两种选择,和 2-SAT 的概念是一样的。
考虑到 地图的数量非常少 (),我们可以试着暴力枚举每个 地图使用的赛车。如果枚举到的情况都不存在解就说明无解,反之输出解即可。时间复杂度为 ,可以接受。
此外在给点编号时操作有点复杂,一定要明确。
int get(int xx, char c, int t) {
char a = s[xx] - 'a';
c -= 'A';
if (((a + 1) % 3 != c) ^ t) return xx + n;
return xx;
}
char put(int xx, int t) {
int yy = s[xx] - 'a';
return 'A' + ((yy + t) % 3);
}
void solve() {
memset(head, -1, sizeof(head));
memset(dfn, 0, sizeof(dfn));
cnt = idx = tot = 0;
for (int i = 0; i < m; i++) {
int xx = op[i].x - 1, yy = op[i].y - 1;
char a = op[i].a, b = op[i].b;
if (s[xx] != tolower(a)) {
if (s[yy] != tolower(b)) {
add(get(xx, a, 0), get(yy, b, 0));
add(get(yy, b, 1), get(xx, a, 1));
} else
add(get(xx, a, 0), get(xx, a, 1));
}
}
for (int i = 0; i < n * 2; i++)
if (!dfn[i]) tarjan(i);
for (int i = 0; i < n; i++)
if (bel[i] == bel[i + n]) return;
for (int i = 0; i < n; i++) {
if (bel[i] < bel[i + n])
cout << put(i, 1);
else
cout << put(i, 2);
}
exit(0);
}
int main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> d >> s;
for (int i = 0, j = 0; i < n; i++)
if (s[i] == 'x') x[j++] = i;
cin >> m;
for (int i = 0; i < m; i++) cin >> op[i].x >> op[i].a >> op[i].y >> op[i].b;
for (int k = 0; k < 1 << d; k++) {
for (int i = 0; i < d; i++)
if (k >> i & 1)
s[x[i]] = 'a';
else
s[x[i]] = 'b';
solve();
}
cout << -1;
return 0;
}
本文作者:Coel's Blog
本文链接:https://www.cnblogs.com/Coel-Flannette/p/16487980.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步