【Coel.学习笔记】2-SAT 问题

终于结束网络流了,真有够累的……

SAT 问题相关概念

SAT 是 \(\mathcal{Satisfaction}\) 的英文缩写,意为“适应性”。对于若干个命题,每个命题都有且只有“真”和“假”两种取值。接下来会给出若干个条件,每个条件都形如“\(x_i\) 为真/假或 \(x_j\) 为真/假或……”的形式,需要找到一个命题取值,使得所有条件都可以得到满足。

如果一个条件关联了 \(k\) 个命题,就称为 K-SAT 问题。特殊地,每个条件只关联 \(2\) 个命题就叫 2-SAT 问题。

对于 \(k>2\) 的情况, SAT 问题是 \(\mathsf{NP}\) 完全的。但当 \(K=2\) 时,我们就可以以线性时间复杂度解决问题。

求解算法

在学习算法之前,我们先要了解一点数理逻辑的知识。


假设一个命题 \(a\) 是命题 \(b\) 的充分条件,即 \(a\to b\),那么就会有 \(a\) 为真时 \(b\) 一定为真,\(a\) 为假时 \(b\) 可能为假。对于式子 \(a\to b\),如果上面的情况正确就为真,反之为假。

为了更形象地了解这个“推导”的含义,下面给出了 \(a,b\) 不同的取值时 \(a\to b\) 的值。

\(a\) \(b\) $$a\to b$$
$$1$$ $$1$$ $$1$$
$$1$$ $$0$$ $$0$$
$$0$$ $$0$$ $$1$$
$$0$$ $$1$$ $$1$$

写 Markdown 的时候上面一坨的 $ 看着好吓人

从上表可以发现, \(a\to b \iff \neg a \lor b\),且 \(a\lor b \iff \neg a \to b \iff \neg b \to a\)


我们可以把这个问题转化为图论问题。把所有命题放在一张有向图上,那么有向图的路径就代表了一个推导关系。接下来只要求解一个强连通分量就好了。

参考代码如下:

// 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

\(N\) 对情侣在这天准备结婚,每对情侣都预先计划好了婚礼举办的时间,其中第 \(i\) 对情侣的婚礼从时刻 \(S_i\) 开始,到时刻 \(T_i\) 结束。婚礼有一个必须的仪式:站在牧师面前聆听上帝的祝福。这个仪式要么在婚礼开始时举行,要么在结束时举行。
\(i\) 对情侣需要 \(D_i\) 分钟完成这个仪式,即必须选择 \(S_i \sim S_i+D_i\)\(T_i-D_i \sim T_i\) 两个时间段之一。
牧师约翰想知道他能否满足每场婚礼的要求,即给每对情侣安排\(S_i \sim S_i+D_i\)\(T_i-D_i \sim T_i\),使得这些仪式的时间段不重叠。
若能满足,还需要帮牧师求出任意一种具体方案。

解析:把每一场婚礼看作一个命题,那么每个命题就有两个取值:在开头做祝福和在结束做祝福。\(O(n^2)\) 枚举一下每个时间段的冲突关系,就可以得到命题的取值。这样就转化成 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 计划进行 \(n\) 场游戏,每场游戏使用一张地图,小 L 会选择一辆车在该地图上完成游戏。
小 L 的赛车有三辆,分别用大写字母 \(A\)\(B\)\(C\) 表示。地图一共有四种,分别用小写字母 \(x\)\(a\)\(b\)\(c\) 表示。
其中,赛车 \(A\) 不适合在地图 \(a\) 上使用,赛车 \(B\) 不适合在地图 \(b\) 上使用,赛车 \(C\) 不适合在地图 \(c\) 上使用,而地图 \(x\) 则适合所有赛车参加。
适合所有赛车参加的地图并不多见,最多只会有 \(d\) 张。
\(n\) 场游戏的地图可以用一个小写字母组成的字符串描述。例如:\(S=\texttt{xaabxcbc}\) 表示小L计划进行 \(8\) 场游戏,其中第 \(1\) 场和第 \(5\) 场的地图类型是 \(x\),适合所有赛车,第 \(2\) 场和第 \(3\) 场的地图是 \(a\),不适合赛车 \(A\),第 \(4\) 场和第 \(7\) 场的地图是 \(b\),不适合赛车 \(B\),第 \(6\) 场和第 \(8\) 场的地图是 \(c\),不适合赛车 \(C\)
小 L 对游戏有一些特殊的要求,这些要求可以用四元组 $ (i, h_i, j, h_j) $ 来描述,表示若在第 \(i\) 场使用型号为 \(h_i\) 的车子,则第 \(j\) 场游戏要使用型号为 \(h_j\) 的车子。
你能帮小 L 选择每场游戏使用的赛车吗?如果有多种方案,输出任意一种方案。如果无解,输出 -1

解析: NOI 的题面和码量都比较大,要认真读题找到内在性质。
如果没有 \(x\) 地图,那么这道题就是一个很简单的 2-SAT 问题,因为每张地图只有两种选择,和 2-SAT 的概念是一样的。
考虑到 \(x\) 地图的数量非常少 (\(d\leq 8\)),我们可以试着暴力枚举每个 \(x\) 地图使用的赛车。如果枚举到的情况都不存在解就说明无解,反之输出解即可。时间复杂度为 \(O(2^d(n+m))\),可以接受。
此外在给点编号时操作有点复杂,一定要明确。

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;
}
posted @ 2022-07-18 20:53  秋泉こあい  阅读(29)  评论(0编辑  收藏  举报