「学习笔记」2-SAT

一.什么是 2-SAT#

SAT 是适定性 Satisfiability 问题的简称。一般形式为 k适定性问题,简称 k-SAT。而当 k>2 时,该问题为 NP 完全的。所以我们只研究 k=2 的情况。

形象地来说,给定 n 个布尔变量 ai,同时给出若干个约束条件:

(not)ai op (not)aj=true/false

而求解 2-SAT 就是求出一组 ai 的解。

二.将 2-SAT 问题转换为图论问题#

首先定义一条有向边的定义 xy:如果满足 x 就必须满足 y

发现一个点有两个状态真或假,可以想到将点 u 拆为点 u0,u1,分别表示假或真。

1. ab=true (a1,b1),(b1,a1)

2. ab=false (a1,b0),(b1,a0)

3. ab=true (a0,b1),(b0,a1)

4. ab=false (a0,b0),(b0,a0)

这样我们就将 2-SAT 问题转换为图论问题了,可以运用图论的方式解决。

三.解决 2-SAT 问题#

  • 1. 判断有无解#

    由上文定义的状态可以得知,u0u1 不能同时被满足。
    那么存在点 x0x1 在同一个强连通分量中时,那么无解。
    反之则有解。

  • 2. 求方案#

    由我们的建边可得:对于每个点 u,选择其拓扑序较大的那种状态更优。
    为什么呢?
    如下图所示:
    image

    对于 x1 的两种状态,如果选择 false,那么推导出 x2true,又推导出 x1true,又推导出 x2false,发现这种解法矛盾了。
    但如果对于 x1 选择 true,那么 x2false,则满足要求。
    由此我们得出:对于每个点 u,选择其拓扑序较大的那种状态更优。
    这里不需要再做一遍拓扑排序求拓扑序,在 Tarjan 求强连通分量中就求出了每个点的逆拓扑序,每个点的强连通分量编号也就是逆拓扑序。

四.例题讲解#

P4782 【模板】2-SAT 问题#

2-SAT 模板题。

按照上文建边,求解即可。

代码如下:

Copy
#include <iostream> #include <cstring> #include <algorithm> #include <cstdio> #include <stack> using namespace std; const int N = 2000005; struct EDGE { int nxt; int to; }e[N]; int cnt = 1, sjc = 0, dfn[N], low[N], col[N], co = 0, head[N], edge_num = 0; int sta[N], top = 0; stack<int> s; bool insta[N]; inline void add_edge (int x, int y) { e[++edge_num].nxt = head[x]; e[edge_num].to = y; head[x] = edge_num; } inline void tarjan (int u) { sta[++top] = u; low[u] = dfn[u] = ++sjc; insta[u] = true; for (int i = head[u] ;~i ; i = e[i].nxt) { int v = e[i].to; if (!dfn[v]) { tarjan (v); low[u] = min (low[u], low[v]); } else if (insta[v]) { low[u] = min (low[u], dfn[v]); } } if (low[u] == dfn[u]) { cnt ++; int tp; do { tp = sta[top--]; col[tp] = cnt; insta[tp] = false; }while (tp != u); } }//Tarjan求强连通分量。 int main() { memset (head, -1, sizeof (head)); int n, m; scanf ("%d%d", &n, &m); while (m --) { int i, a, j, b; scanf ("%d%d%d%d", &i, &a, &j, &b); if (a == 0 && b == 0) {//a=0或b=0 add_edge (i + n, j);//i真j假 add_edge (j + n, i);//j真i假 } if (a == 0 && b == 1) {//a=0或b=1 add_edge (i + n, j + n);//i真j假 add_edge (j, i);//j假i真 } if (a == 1 && b == 0) {//a=1或b=0 add_edge (i, j);//i假j假 add_edge (j + n, i + n);//j真i真 } if (a == 1 && b == 1) {//a=1或b=1 add_edge (i, j + n);//i假j真 add_edge (j, i + n);//j假i真 } } for (int i = 1; i <= (n << 1); i ++) { if (!dfn[i]) { tarjan (i); } } for (int i = 1; i <= n; i ++) { if (col[i] == col[i + n]) {//无解状态。 puts ("IMPOSSIBLE"); return 0; } } puts ("POSSIBLE"); for (int i = 1; i <= n; i ++) { printf ("%d ", col[i] > col[i + n] ? 1 : 0); //选择拓扑序大的。 } return 0; }

P4171 [JSOI2010] 满汉全席#

设汉式的点编号为 1n,满式的点编号为 n+12n

那么就按照模板建边解决 2-SAT 问题即可。

UVA11294 Wedding#

题意:有 n 对夫妻分别坐在新郎与新娘两侧,有两个要求:

1. 一对夫妻不能坐在同一侧;

2. 给出 m 个特殊关系,有特殊关系的两个人不能同时坐在新郎一侧,也就是说可以分别坐两侧,或者同时坐在新娘一侧。

我们设女方的 0 状态为 1n,男方的 0 状态为 n+12n,女方的 1 状态为 2n+13n,男方的 1 状态为 3n+14n

同时设 0 状态在左边,也就是新娘一侧。1 状态在右边,也就是新郎一侧。

这样就可以建边了解决 2-SAT 问题了。

建边代码如下:

Copy
Add(1 + 2 * n, 1);//强制新娘在左边 Add(1 + n, 1 + n + 2 * n);//强制新郎在右边 for (int i = 2; i <= n; ++i) { int x = i, y = i + n; //x女方0状态,y男方0状态 Add(x, y + 2 * n); Add(y + 2 * n, x); Add(x + 2 * n, y); Add(y, x + 2 * n); //双方0状态与1状态互相连边 } while (m --) { int x, y; char a, b; scanf("%d%c%d%c", &x, &a, &y, &b); x ++, y ++;//调整编号为1-n if (a == 'h') { x += n; } if (b == 'h') { y += n; } //男方要加一个n Add(x + 2 * n, y); Add(y + 2 * n, x); //双方不在同一侧 }

P3007 [USACO11JAN] The Continental Cowngress G#

也是模板。

拆点后建边跑 Tarjan,然后用 dfs 判断点的可行情况即可。

posted @   cyhyyds  阅读(68)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示
CONTENTS