「笔记」2-SAT

写在前面

草草草疫情期间我都在家水了些啥啊啊啊啊= =
蛮有意思的建模技巧,对图论有了新的认识。

引入

k-SAT(Satisfiability)问题的形式如下:

\(n\) 个 01 变量 \(x_1\sim x_n\),另有 \(m\) 个变量取值需要满足的限制。
每个限制是一个 \(k\) 元组 \((x_{p_1}, x_{p_2}, \cdots, x_{p_k})\),满足 \(x_{p_1}\oplus x_{p_2}\oplus\cdots\oplus x_{p_k} = a\)。其中 \(a\)\(0/1\)\(\oplus\) 是某种二元 bool 运算。
要求构造一种满足所有限制的变量的赋值方案。

\(k > 2\) 的情况是 NP 完全的,只能暴力求解。而 2-SAT 问题可以通过建立图论模型,在 \(O(n+m)\) 的时间复杂度内判断是否有解,若有解可以构造出一组合法解。

分析

不同题目中的二元 bool 运算可能有差异,但建图的思路相同。以 P4782 【模板】2-SAT 问题 为例:

\(n\) 个布尔变量 \(x_1\sim x_n\),另有 \(m\) 个需要满足的条件,每个条件的形式都是 「\(x_i\) 为 true / false 或 \(x_j\) 为 true / false」。
要求判断是否有解,若有解可以构造出一组合法解。
\(1\le n,m\le 10^6\)
1S,512MB。

首先将变量 \(x_i\) 拆成两个节点 \(i\)\(i+n\),分别代表事件 \(x_i =1/0\)。显然两个事件是互斥的。
对于限制条件「\(a\)\(b\)」(\(a\)\(b\) 是两个 \(x_i = y\) 形式的命题),两个命题中至少有一个为真,于是可将其拆成两个充分条件形式的限制:\(\lnot a\to b\)\(\lnot b\to a\)
根据上述充分条件,建立图论模型,从节点 \(\lnot a\) 向节点 \(b\) 连边,从节点 \(\lnot b\) 向节点 \(a\) 连边。

之后考察节点 \(i\)\(i + n\) 在图中的关系。若它们互相可达,即在同一个强连通分量中,说明在赋值限制下,它们代表的一对互斥事件会同时发生。则不存在一组合法的赋值方案。
否则说明有解,考虑如何构造一组合法解。
考虑先对建出的图进行缩点得到一张 DAG。对于变量 \(x_i\),考察节点 \(i\)\(i+n\) 所在强连通分量的拓扑关系。若两分量不连通,则 \(x_i\) 可取任意一个值。否则只能取属于拓扑序较大的分量的值。因为若取拓扑序较小的值,可以根据逻辑关系推出取另一个值也是同时发生的。

可以使用 Tarjan/Kosaraju 算法求得强连通分量。值得注意的是 Tarjan 算法赋给强连通分量的编号顺序与拓扑序是相反的,输出时的判断条件要反着写。

代码

P4782 【模板】2-SAT 问题 的代码,使用 Tarjan 实现。

//知识点:2-SAT
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <stack>
#define LL long long
const int kN = 2e6 + 10;
//=============================================================
int n, m, e_num, head[kN], v[kN << 1], ne[kN << 1];
int dfn_num, bel_num, dfn[kN], low[kN], bel[kN];
std::stack <int> st;
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
void Add(int u_, int v_) {
  v[++ e_num] = v_;
  ne[e_num] = head[u_];
  head[u_] = e_num;
}
void Tarjan(int u_) {
  dfn[u_] = low[u_] = ++ dfn_num;
  st.push(u_);
  for (int i = head[u_]; i; i = ne[i]) {
    int v_ = v[i];
    if (!dfn[v_]) {
      Tarjan(v_);
      Chkmin(low[u_], low[v_]);
    } else if (!bel[v_]) {
      Chkmin(low[u_], dfn[v_]);
    }
  }
  if (dfn[u_] == low[u_]) {
    ++ bel_num;
    while (true) {
      int top = st.top(); st.pop();
      bel[top] = bel_num;
      if (top == u_) break;
    } 
  }
}
//=============================================================
int main() { 
  n = read(), m = read();
  for (int i = 1; i <= m; ++ i) {
    int x = read(), a = read(), y = read(), b = read();
    if (a && b) Add(x + n, y), Add(y + n, x);
    if (!a && b) Add(x, y), Add(y + n, x + n);
    if (a && !b) Add(x + n, y + n), Add(y, x);
    if (!a && !b) Add(x, y + n), Add(y, x + n);
  }
  for (int i = 1; i <= 2 * n; ++ i) {
    if (!dfn[i]) Tarjan(i);
    if (i <= n && bel[i] == bel[i + n]) {
      printf("IMPOSSIBLE\n");
      return 0;
    }
  }
  printf("POSSIBLE\n");
  for (int i = 1; i <= n; ++ i) printf("%d ", bel[i] < bel[i + n]); //Tarjan 算法赋给强连通分量的编号顺序与拓扑序相反
  return 0; 
}

例题

板题:P4171 [JSOI2010]满汉全席P5782 [POI2001]和平委员会

写在最后

鸣谢:

《算法竞赛进阶指南》
题解 P4782 【【模板】2-SAT 问题】 - Great_Influence 的博客
2-SAT - OI Wiki

posted @ 2021-01-21 17:40  Luckyblock  阅读(141)  评论(2编辑  收藏  举报