定义
SAT 是适定性(Satisfiability)问题的简称。一般形式为 k - 适
定性问题,简称 k-SAT。而当 k > 2 时该问题为 NP 完全的。
所以我们之研究 k = 2 的情况。
2-SAT,简单的说就是给出 n 个集合,每个集合有两个元素,已知若
干个 < a,b > ,表示 a 与 b 矛盾(其中 a 与 b 属于不同的集
合)。然后从每个集合选择一个元素,判断能否一共选 n 个两两不
矛盾的元素。显然可能有多种选择方案,一般题中只需要求出一种
即可。
常用解决方法
TarjanSCC 缩点
算法考究在建图这点,我们举个例子来讲:
假设有 a1,a2 和 b1,b2 两对,已知 a1 和 b2 间有矛盾,于是为了
方案自洽,由于两者中必须选一个,所以我们就要拉两条条有向边
(a1,b1) 和 (b2,a2) 表示选了 a1 则必须选 b1 ,选了 b2 则必须选
a2 才能够自洽。
然后通过这样子建边我们跑一遍 Tarjan SCC 判断是否有一个集合中
的两个元素在同一个 SCC 中,若有则输出不可能,否则输出方案。
构造方案只需要把几个不矛盾的 SCC 拼起来就好了。
输出方案时可以通过变量在图中的拓扑序确定该变量的取值。如果
变量 ¬x 的拓扑序在 x 之后,那么取 x 值为真。应用到 Tarjan 算法
的缩点,即 x 所在 SCC 编号在 ¬x 之前时,取 x 为真。因为 Tarjan
算法求强连通分量时使用了栈,所以 Tarjan 求得的 SCC 编号相当于
反拓扑序。
显然地,时间复杂度为 O(n + m) 。
暴搜及模板
就是沿着图上一条路径,如果一个点被选择了,那么这条路径以后
的点都将被选择,那么,出现不可行的情况就是,存在一个集合中
两者都被选择了。
那么,我们只需要枚举一下就可以了,数据不大,答案总是可以出
来的。
// 来源:刘汝佳白书第 323 页
struct Twosat {
int n;
vector<int> g[maxn * 2];
bool mark[maxn * 2];
int s[maxn * 2], c;
bool dfs(int x) {
if (mark[x ^ 1]) return false;
if (mark[x]) return true;
mark[x] = true;
s[c++] = x;
for (int i = 0; i < (int)g[x].size(); i++)
if (!dfs(g[x][i])) return false;
return true;
}
void init(int n) {
this->n = n;
for (int i = 0; i < n * 2; i++) g[i].clear();
memset(mark, 0, sizeof(mark));
}
void add_clause(int x, int y) { // 这个函数随题意变化
g[x].push_back(y ^ 1); // 选了 x 就必须选 y^1
g[y].push_back(x ^ 1);
}
bool solve() {
for (int i = 0; i < n * 2; i += 2)
if (!mark[i] && !mark[i + 1]) {
c = 0;
if (!dfs(i)) {
while (c > 0) mark[s[--c]] = false;
if (!dfs(i + 1)) return false;
}
}
return true;
}
};
例题
1、 Party
有n对夫妻被邀请参加一个聚会,因为场地的问题,每对夫妻中只有1人可以列席。在2n 个人中,某些人之间有着很大的矛盾(当然夫妻之间是没有矛盾的),有矛盾的2个人是不会同时出现在聚会上的。有没有可能会有n 个人同时列席?
Input
n: 表示有n对夫妻被邀请 (n<= 1000)
m: 表示有m 对矛盾关系 ( m < (n - 1) * (n -1))
在接下来的m行中,每行会有4个数字,分别是 A1,A2,C1,C2
A1,A2分别表示是夫妻的编号
C1,C2 表示是妻子还是丈夫 ,0表示妻子 ,1是丈夫
夫妻编号从 0 到 n -1
Output
如果存在一种情况 则输出YES
否则输出 NO
Sample Input
2
1
0 1 1 1
Sample Output
YES
#include<iostream>
#include<vector>
#include<string>
#include<stdio.h>
#include<cstring>
using namespace std;
const int maxn=1000+10;
struct Twosat
{
int n;
vector<int> g[maxn*2];
int s[maxn*2],c;
bool mark[maxn*2];
bool dfs(int x){
if(mark[x^1])//此条件的反存在,不成立
return false;
if(mark[x])//当此条件存在,则成立
return true;
mark[x]=true;//假设此条件成立
s[c++]=x;//加入假设的条件
for(int i=0;i<(int)g[x].size();i++){//此条件能推出的各种条件
if(!dfs(g[x][i]))return false;
}
return true;
}
void init(int n)
{
this->n=n;
for(int i=0;i<n*2;i++)
g[i].clear();
memset(mark,0,sizeof(mark));
}
// void(add_clause(int x,int y))
// {
// g[x].push_back(y^1);//选x必须选y^1
// g[y].push_back(x^1);
// }
void add_clause(int x,int xval,int y,int yval)
{
x=x*2+xval;
y=y*2+yval;
g[x].push_back(y);
}
bool solve()
{
for(int i=0;i<n*2;i+=2)
{
if(!mark[i]&&!mark[i+1])
{
c=0;
if(!dfs(i))
{
while(c>0)mark[s[--c]]=false;//因为在深搜的时候加入了假设条件
if(!dfs(i+1))return false;
}
}
}
return true;
}
}Ts;
int main(){
int n,m;
while(scanf("%d%d",&n,&m)==2)
{
Ts.init(n);
while(m--)
{
int a,b,va,vb;
scanf("%d%d%d%d",&a,&b,&va,&vb);
Ts.add_clause(a,va,b,vb^1);
Ts.add_clause(b,vb,a,va^1);//因为关系矛盾是双方的
}
printf("%s\n",Ts.solve()?"YES":"NO");
}
return 0;
}