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

2-SAT,简单的说就是给出 \(n\) 个集合,每个集合有两个元素,已知若干个 \(<a,b>\),表示 \(a\) 与 \(b\) 矛盾(其中 \(a\) 与 \(b\) 属于不同的集合)。然后从每个集合选择一个元素,判断能否一共选 \(n\) 个两两不矛盾的元素。显然可能有多种选择方案,一般题中只需要求出一种即可。

建图

我们将 \(n\) 个集合拆成两个点,分别代表着 truefalse.

a || b == true 时,我们可以得知:

a == false 时,b = true

b == false 时,a = true;

这两个关系存在因果关系;

a == true 时,我们不能得知 b = true 还是 b = false,这两者之间不存在因果关系,b == true 同理。

因此,我们将 \(a_0\) 向 \(b_1\) 连边,将 \(b_0\) 向 \(a_1\) 连边。

含义:

a == false 时,b = true

b == false 时,a = true

a && b == false 时,我们可以得知:

a == true 时,b = false;

b == true 时,a = false;

我们将 \(a_1\) 向 \(b_0\) 连边,将 \(b_1\) 向 \(a_0\) 连边。

含义:

a == true 时,b = false;

b == true 时,a = false

a && b == true 时,我们发现,a == trueb == true,除了这种情况不会再有其他情况了,即 \(a\) 的值一定为 true,\(b\) 的值一定为 true

这种情况下,我们将 \(a_0\) 向 \(a_1\) 连边,\(b_0\) 向 \(b_1\) 连边。

含义:

a == false 时,a = true;(即 \(a\) 一定不为 false

b == false 时,b = true。(即 \(b\) 一定不为 true)

判断是否有解

如果 \(a_0\) 可以到达 \(a_1\),说明 \(a\) 一定为 true

如果 \(a_1\) 可以到达 \(a_0\),说明 \(a\) 一定为 false

判断是否有解即在一种情况中 \(a\) 都有唯一确定的值,要么为 true,要么为 false,倘若在同一种情况中,\(a_0\) 可以到达 \(a_1\), \(a_1\) 可以到达 \(a_0\),则无法确定 \(a\) 的值,此情况下无解,即 \(a_0\) 与 \(a_1\) 在同一个强连通分量里。

用 tarjan 算法来找强连通分量即可。

题目

P4782 【模板】2-SAT 问题

#include <bits/stdc++.h>
using namespace std;
typedef long long ll; const int N = 1e6 + 5; int n, m, tim, scc;
vector<int> son[N << 1], sta;
int dfn[N << 1], low[N << 1], lt[N << 1]; void tarjan(int u) {
dfn[u] = low[u] = ++ tim;
sta.push_back(u);
for (int v : son[u]) {
if (!dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
}
else if (!lt[v]) {
low[u] = min(low[u], dfn[v]);
}
}
if (dfn[u] == low[u]) {
lt[u] = ++ scc;
while (sta.back() != u) {
lt[sta.back()] = scc;
sta.pop_back();
}
sta.pop_back();
}
} int main() {
scanf("%d%d", &n, &m);
while (m --) {
int xi, i, xj, j;
scanf("%d%d%d%d", &xi, &i, &xj, &j);
son[xi + n * (i & 1)].push_back(xj + (j ^ 1) * n);
son[xj + n * (j & 1)].push_back(xi + (i ^ 1) * n);
}
for (int i = 1; i <= (n << 1); ++ i) {
if (!dfn[i]) {
tarjan(i);
}
}
for (int i = 1; i <= n; ++ i) {
if (lt[i] == lt[i + n]) {
puts("IMPOSSIBLE");
return 0;
}
}
puts("POSSIBLE");
for (int i = 1; i <= n; ++ i) {
printf("%d%c", (lt[i] < lt[i + n]), " \n"[i == n]);
}
return 0;
}

CF1475F

将 \(A\) 和 \(B\) 两个矩阵异或,得到一个新矩阵 \(C\),若 \(C\) 可以通过异或行或列的操作来变成全 \(0\) 矩阵,那么说明 \(A\) 可以通过异或得到 \(B\)。

我们发现,每一行或每一列要么不异或,要么异或一次,由此可以想到 2-SAT。

对于 \(C\) 中的元素,若 \(C(i, j)\) 为 \(1\),则要么行异或,要么列异或;若 \(C(i, j)\) 为 \(0\),则要么行和列都异或,要么行和列都不异或,由此建边判断是否有解。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll; const int N = 1005; int T, n, cnt, tim, scc;
int a[N][N], b[N][N], c[N][N], x[N][2], y[N][2];
int dfn[N << 2], low[N << 2], lt[N << 2];
vector<int> son[N << 2], Stack; void init() {
cnt = tim = scc = 0;
for (int i = 1; i <= n; ++ i) {
x[i][1] = ++ cnt;
son[cnt].clear();
dfn[cnt] = lt[cnt] = 0;
}
for (int i = 1; i <= n; ++ i) {
x[i][0] = ++ cnt;
son[cnt].clear();
dfn[cnt] = lt[cnt] = 0;
}
for (int i = 1; i <= n; ++ i) {
y[i][1] = ++ cnt;
son[cnt].clear();
dfn[cnt] = lt[cnt] = 0;
}
for (int i = 1; i <= n; ++ i) {
y[i][0] = ++ cnt;
son[cnt].clear();
dfn[cnt] = lt[cnt] = 0;
}
} void tarjan(int u) {
dfn[u] = low[u] = ++ tim;
Stack.push_back(u);
for (int v : son[u]) {
if (! dfn[v]) {
tarjan(v);
low[u] = min(low[u], low[v]);
}
else if (! lt[v]) {
low[u] = min(low[u], dfn[v]);
}
}
if (dfn[u] == low[u]) {
lt[u] = ++ scc;
while (Stack.back() != u) {
lt[Stack.back()] = scc;
Stack.pop_back();
}
Stack.pop_back();
}
} int main() {
scanf("%d", &T);
while (T --) {
scanf("%d", &n);
init();
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= n; ++ j) {
scanf("%1d", &a[i][j]);
}
}
for (int i = 1; i <= n; ++ i) {
for (int j = 1; j <= n; ++ j) {
scanf("%1d", &b[i][j]);
c[i][j] = a[i][j] ^ b[i][j];
if (c[i][j]) {
son[x[i][1]].push_back(y[j][0]);
son[y[j][0]].push_back(x[i][1]);
son[x[i][0]].push_back(y[j][1]);
son[y[j][1]].push_back(x[i][0]);
}
else {
son[x[i][1]].push_back(y[j][1]);
son[y[j][1]].push_back(x[i][1]);
son[x[i][0]].push_back(y[j][0]);
son[y[j][0]].push_back(x[i][0]);
}
}
}
for (int i = 1; i <= cnt; ++ i) {
if (! dfn[i]) {
tarjan(i);
}
}
int fg = 0;
for (int i = 1; i <= n; ++ i) {
if (lt[x[i][1]] == lt[x[i][0]]) {
puts("NO");
fg = 1;
break;
}
if (lt[y[i][1]] == lt[y[i][0]]) {
puts("NO");
fg = 1;
break;
}
}
if (! fg) puts("YES");
}
return 0;
}

「学习笔记」2-SAT问题的更多相关文章

  1. 「学习笔记」Min25筛

    「学习笔记」Min25筛 前言 周指导今天模拟赛五分钟秒第一题,十分钟说第二题是 \(\text{Min25}​\) 筛板子题,要不是第三题出题人数据范围给错了,周指导十五分钟就 \(\text{AK ...

  2. 「学习笔记」FFT 之优化——NTT

    目录 「学习笔记」FFT 之优化--NTT 前言 引入 快速数论变换--NTT 一些引申问题及解决方法 三模数 NTT 拆系数 FFT (MTT) 「学习笔记」FFT 之优化--NTT 前言 \(NT ...

  3. 「学习笔记」FFT 快速傅里叶变换

    目录 「学习笔记」FFT 快速傅里叶变换 啥是 FFT 呀?它可以干什么? 必备芝士 点值表示 复数 傅立叶正变换 傅里叶逆变换 FFT 的代码实现 还会有的 NTT 和三模数 NTT... 「学习笔 ...

  4. 「学习笔记」Treap

    「学习笔记」Treap 前言 什么是 Treap ? 二叉搜索树 (Binary Search Tree/Binary Sort Tree/BST) 基础定义 查找元素 插入元素 删除元素 查找后继 ...

  5. 「学习笔记」字符串基础:Hash,KMP与Trie

    「学习笔记」字符串基础:Hash,KMP与Trie 点击查看目录 目录 「学习笔记」字符串基础:Hash,KMP与Trie Hash 算法 代码 KMP 算法 前置知识:\(\text{Border} ...

  6. 「学习笔记」平衡树基础:Splay 和 Treap

    「学习笔记」平衡树基础:Splay 和 Treap 点击查看目录 目录 「学习笔记」平衡树基础:Splay 和 Treap 知识点 平衡树概述 Splay 旋转操作 Splay 操作 插入 \(x\) ...

  7. 「学习笔记」wqs二分/dp凸优化

    [学习笔记]wqs二分/DP凸优化 从一个经典问题谈起: 有一个长度为 \(n\) 的序列 \(a\),要求找出恰好 \(k\) 个不相交的连续子序列,使得这 \(k\) 个序列的和最大 \(1 \l ...

  8. 「学习笔记」ST表

    问题引入 先让我们看一个简单的问题,有N个元素,Q次操作,每次操作需要求出一段区间内的最大/小值. 这就是著名的RMQ问题. RMQ问题的解法有很多,如线段树.单调队列(某些情况下).ST表等.这里主 ...

  9. 「学习笔记」递推 & 递归

    引入 假设我们想计算 \(f(x) = x!\).除了简单的 for 循环,我们也可以使用递归. 递归是什么意思呢?我们可以把 \(f(x)\) 用 \(f(x - 1)\) 表示,即 \(f(x) ...

  10. 「学习笔记」动态规划 I『初识DP』

    写在前面 注意:此文章仅供参考,如发现有误请及时告知. 更新日期:2018/3/16,2018/12/03 动态规划介绍 动态规划,简称DP(Dynamic Programming) 简介1 简介2 ...

随机推荐

  1. 修改密码 MVC

    控制器site public function actionPassword(){ $model = new PasswordForm(); /*判断请求属性 if ($request->isA ...

  2. PHP 合并两个二维数组

    思路:遍历二维数组,合并两个二维数组的值,赋值给新数组 function mergeArray($arr1,$arr2){ $newArr = []; foreach($arr1 as $v1){ f ...

  3. Undelivered Mail Returned to Sender

    电子邮件是使用电子邮件地址application@sample.com从Application发送的. 出于业务原因我们无法在此应用程序中更新/删除无效的外部电子邮件地址,因此响应出站电子邮件会生成许 ...

  4. Python学习笔记--文件的相关操作

    文件的读取操作 读操作 实现: read()--读完 read(10)--读取10个字节 readline()--将所有行并到一行输出 readlines()--一次读取一行 文件的关闭: 实现: 上 ...

  5. Spring--案例:百度网盘密码数据兼容处理

    案例再度来袭 也就是说,在百度网盘的密码复制时,后面即使有空格也能提取成功(trim方法) 案例的实现: 也就是实现存在空格时,也能输出true: 现在的话: 那么,我们应该如何使得它忽略空格呢? 这 ...

  6. 如何让excel不转换科学技术法

    使用场景: 业务部门从系统导出数据给开发人员,打开后数字全部变为科学计数法 参考文章:https://www.zhihu.com/question/20096750

  7. 2020 ccpc秦皇岛 赛后总结!!!!

    amazing!!!! 金牌!!!!! 总结一下这次的发挥,以及如何冲到了金牌. 1 有队友单开了银牌题,50分钟过了K题,当时只有5个人过K.他敲的过程中另个队友想出来另外一题的思路,等过了K,我直 ...

  8. 搭建良好编写体验的webgl编程环境 vscode+vit

    因为webgl代码是以字符串的形式嵌入在javascript代码中,这对于我们编写webgl代码的体验不友好,本文介绍如何搭建友好webgl编程环境: 需要安装的vscode插件 WebGL GLSL ...

  9. Spring 注解整理

    更多内容,前往IT-BLOG 一.核心容器 [1]@Configuration:告诉 Spring 这是一个配置类(配置类=配置文件)[2]@Bean:给容器中注册一个 Bean :类型为返回值类型, ...

  10. Java面试——锁

    公平锁:是指多个线程按照申请锁的顺序来获取锁,有点先来后到的意思.在并发环境中,每个线程在获取锁时会先查看此锁维护的队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中, ...