这篇文章上次修改于 342 天前,可能其部分内容已经发生变化,如有疑问可询问作者。
ACM程序课算法笔记16——2-SAT
定义
SAT 是适定性(Satisfiability)问题的简称。一般形式为 k - 适定性问题,简称 k-SAT。 对于n个集合,每个集合仅有两个元素。有存在元素矛盾,两个元素不能一起出现,两个元素不属于一个集合,问每个集合中选一个元素,判断能否选n个。找到其中的一个解决方案。
换个说法,有n个元素,每个元素都有两种状态,期中存在一些约束关系,比如A处于第一种状态的时候,B不能处于第二种状态。问是否能不冲突成立。
也就是存在多个布尔表达式,问是否存在情况使所有布尔表达式为真。
分析
为了便于解释,我们将每个元素的两种状态称为正反。
有点类似于小学奥数中推理问题,比如有条件A是正,B不能是正,我们就可以进行假设判定。如果A取正,那么B就只能取反。这就是一个推理过程,然后从B往后推,一旦推到A必须为反,就与假设矛盾了,也就不成立。
例如有条件:
种类①:A为正,即条件 A反 → A正 (直接使A必定为正)
种类②:A正时B不能为正,即条件 A正 → B反 , B正 → A反
种类③:A == B 即条件 A正 → B正 ,A反 → B反,B正 → A正,B反 → A反
种类④:A ≠ B 即条件 A正 → B反 ,A反 → B正,B正 → A反,B反 → A正
注意!所有关系其逆否命题也必定成立,不能漏掉
建图
这就是一个推导条件,建立这些为边。
对于N个元素,图中有2N个点,这些点之间如果有这种推导关系就是建立了一个有向边
对于任意一个点,一旦我选择了其中一个点,这个点之后的路径走到的点就都要被取。
一旦取到的点属于同一个元素,说明当前取点不成立。
思路
暴搜
就是沿着图上一条路径,如果一个点被选择了,那么这条路径以后的点都将被选择,那么,出现不可行的情况就是,存在一个集合中两者都被选择了。
强联通分量缩点
引入强联通分量的概念,强联通分量在这个图中的意义是,属于一个强联通分量的点,一旦取了其中一个,那么其中所有的点都要被取。
先对图进行缩点,一旦一个缩点点集里面存在同一个元素的正反,就代表无解。
但如果没有这种冲突,该图就必定有解。
为什么必定有解
我们不妨探寻一下这个图的规律,由于种类①的边很有特色,我们不妨先排除这个种类。剩下的种类都有一个特点,其逆否命题和原命题组成了一对,而且逆否命题和原命题的边,两边的点均不一样。
如果我们多加几个点,这个图的样貌就很有特色
graph TB
A1 --> B1
B2 --> A2
C1 --> A1
A2 --> C2
D1 --> A1
A2 --> D2
B1 --> E1
E2 --> B2
B1 -.-> C2
C1 -.-> B2
这是一个关系的设定,我们的前提条件是已经缩了所有的环,所以此处无环。
由于逆否命题的特性:
- 该图必定有对称性。
- 所以对于任意一点例如A1A2,A1的后继和A2的前置必定对称
- 若问题无解,必定存在A1A2属于同一个环
根据对称性可证明,如果缩点点集不存在同一元素,则必定有解。
再回头考虑第一种联系,第一种联系是一个单向边,只要取被指向的那一组即可。
算法解决
- 先进行强联通分量的处理,判断缩点点集是否存在同一元素,如果没有,则有解。
- 如何得出结论,由拓扑排序的从后往前取即可
例题解决
#include<bits/stdc++.h>
using namespace std;
void solve(int n, int m){
vector<int> G1[2 * n];// 图关系
vector<int> G2[2 * n]; // 反图
for(int i = 0; i < m; i++){
int cuple1, cuple2, isHus1, isHus2;
int oth1, oth2;
cin >> cuple1 >> cuple2 >> isHus1 >> isHus2;
int person1 = cuple1 * 2 + isHus1;
int person2 = cuple2 * 2 + isHus2;
if(isHus1 == 0)
oth1 = person1 + 1;
else
oth1 = person1 - 1;
if(isHus2 == 0)
oth2 = person2 + 1;
else
oth2 = person2 - 1;
G1[person1].push_back(oth2);
G1[person2].push_back(oth1);
G2[oth2].push_back(person1);
G2[oth1].push_back(person2);
}
vector<int> order;
vector<bool> visited(2 * n);
vector<int> sccValue(2 * n);
function<void(int)> dfs1 = [&](int u){
visited[u]= true;
for(int v : G1[u]){
if(not visited[v]) dfs1(v);
}
order.push_back(u);
};
for (int i = 0; i < 2 * n; ++i)
if (!visited[i]) dfs1(i);
int sccCnt = 0;
function<void(int)> dfs2 = [&](int u){
sccValue[u] = sccCnt;
for(int v : G2[u]){
if(not sccValue[v]) dfs2(v);
}
};
for (int i = 2 * n - 1; i >= 0; i--){
if (!sccValue[order[i]]) {
++sccCnt;
dfs2(order[i]);
}
}
bool success = true;
for(int i = 0; i < n; i++){
if(sccValue[2 * i] == sccValue[2 * i + 1]){
success = false;
break;
}
}
if(success){
cout << "YES" << endl;
}else{
cout << "NO" << endl;
}
}
int main(){
int n, m;
while(cin >> n >> m){
solve(n, m);
}
}