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. 3DMAX2018安装

    1.下载3DMAX2018安装包并解压 2.打开解压后的文件点击Setup 选择语言和安装位置点击下一步 安装完成后点击enter a serial number 输入序列号066-66666666, ...

  2. win10开启休眠

    powercfg /hibernate on 管理员模式下的命令提示符

  3. WSL安装Ubuntu 22.04 (1)

    1. 安装WSL WSL是适用于 Linux 的 Windows 子系统可让开发人员按原样运行 GNU/Linux 环境 - 包括大多数命令行工具.实用工具和应用程序 - 且不会产生传统虚拟机或双启动 ...

  4. rename基本操作

    电脑是Macbook, 用Homebrew先安装rename. 如果没安装Homebrew 直接复制到terminal中回车, 时间稍长. ruby -e "$(curl -fsSL htt ...

  5. pushd 和 popd:对目录栈进行操作

    介绍 目录栈是保存目录的栈结构,当前目录处于该栈结构的顶端,可使用dirs查看目录栈的目录.pushd命令可添加一个目录到目录栈,popd命令会清除目录栈中的一个目录. dirs dirs有三个参数: ...

  6. 基于 Istio 的灰度发布架构方案实践之路

    作者:京东物流 赵勇萍 1. 背景介绍 灰度发布,又名金丝雀发布,是指能够平滑过渡的一种发布方式.基于系统稳定性和快速业务迭代的综合考虑,业务应用开发团队采取了新版本服务灰度上线的方式,即新版本服务并 ...

  7. 推荐一套轻量级的开源图床系统:Light Fast Picture

    如果您跟我一样平时有些博客的习惯,那么图片存储是否有困扰过你呢?今天就给大家推荐一款不错的开源图床系统:Light Fast Picture 它是一个基于koa + vue3.x + typescri ...

  8. SpringBoot笔记--文件配置加载顺序+整合其他框架

    内部文件配置加载顺序 外部文件配置加载顺序 jar包配置 整合Junit 若是业务管理类和测试类在同一个包下面,那么这句话, 可以不加括号,只写注解名称 否则,就必须指定到包下面,不然会报错 整合Re ...

  9. 能让Java开发者提高效率的10个工具

    ​ Java受到全球百万计开发者的追捧,已经演变为一门出色的编程语言.最终,这门语言随着技术的变化,不断的被改善以迎合变化的市场需求. 无论你是否拥有一家科技公司,软件已经成为几乎每一个企业不可或缺的 ...

  10. PHP 图片的合并,微信小程序码合并,文字合并

    //业务需求:我们需要一个微信小程序码,但是是需要提供给别人扫码的但是只有一个纯粹的小程序码是不好看的,所以需要推广的海报图片.再结合文字 最终效果 准备工作  1.需要海报的底图  2.小程序码的图 ...