oi-wiki
Version:
wiki for OI / ACM-ICPC
166 lines (141 loc) • 5.7 kB
Markdown
> SAT 是适定性( Satisfiability )问题的简称 。一般形式为 k - 适定性问题,简称 k-SAT 。而当 $k>2$ 时该问题为 NP 完全的。所以我们之研究 $k=2$ 的情况。
2-SAT ,简单的说就是给出 $n$ 个集合,每个集合有两个元素,已知若干个 $<a,b>$ ,表示 $a$ 与 $b$ 矛盾(其中 $a$ 与 $b$ 属于不同的集合)。然后从每个集合选择一个元素,判断能否一共选 $n$ 个两两不矛盾的元素。显然可能有多种选择方案,一般题中只需要求出一种即可。
比如邀请人来吃喜酒,夫妻二人必须去一个,然而某些人之间有矛盾(比如 A 先生与 B 女士有矛盾,C 女士不想和 D 先生在一起),那么我们要确定能否避免来人之间没有矛盾,有时需要方案。这是一类生活中常见的问题。
算法考究在建图这点,我们举个例子来讲:
假设有 ${a1,a2}$ 和 ${b1,b2}$ 两对,已知 $a1$ 和 $b2$ 间有矛盾,于是为了方案自洽,由于两者中必须选一个,所以我们就要拉两条条有向边 $(a1,b1)$ 和 $(b2,a2)$ 表示选了 $a1$ 则必须选 $b1$ ,选了 $b2$ 则必须选 $a2$ 才能够自洽。
然后通过这样子建边我们跑一遍 Tarjan SCC 判断是否有一个集合中的两个元素在同一个 SCC 中,若有则输出不可能,否则输出方案。构造方案只需要把几个不矛盾的 SCC 拼起来就好了。
显然地, 时间复杂度是 $O(n+m)$ 的,即缩点的复杂度。
就是沿着图上一条路径,如果一个点被选择了,那么这条路径以后的点都将被选择,那么,出现不可行的情况就是,存在一个集合中两者都被选择了。
那么,我们只需要枚举一下就可以了,数据不大,答案总是可以出来的。
下方代码来自刘汝佳的白书:
```cpp
//来源:白书第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;
}
};
```
> 题面:有 n 对夫妻被邀请参加一个聚会,因为场地的问题,每对夫妻中只有 $1$ 人可以列席。在 $2n$ 个人中,某些人之间有着很大的矛盾(当然夫妻之间是没有矛盾的),有矛盾的 $2$ 个人是不会同时出现在聚会上的。有没有可能会有 $n$ 个人同时列席?
这是一道多校题,裸的 2-SAT 判断是否有方案,按照我们上面的分析,如果 $a1$ 中的丈夫和 $a2$ 中的妻子不合,我们就把 $a1$ 中的丈夫和 $a2$ 中的丈夫连边,把 $a2$ 中的妻子和 $a1$ 中的妻子连边,然后缩点染色判断即可。
```cpp
//作者:小黑AWM
using namespace std;
int Index,instack[maxn],DFN[maxn],LOW[maxn];
int tot,color[maxn];
int numedge,head[maxn];
struct Edge{
int nxt,to;
}edge[maxm];
int sta[maxn],top;
int n,m;
void add(int x,int y){
edge[++numedge].to=y;
edge[numedge].nxt=head[x];
head[x]=numedge;
}
void tarjan(int x){//缩点看不懂请移步强连通分量上面有一个链接可以点。
sta[++top]=x;
instack[x]=1;
DFN[x]=LOW[x]=++Index;
for(int i=head[x];i;i=edge[i].nxt){
int v=edge[i].to;
if(!DFN[v]){
tarjan(v);
LOW[x]=min(LOW[x],LOW[v]);
}
else if(instack[v])
LOW[x]=min(LOW[x],DFN[v]);
}
if(DFN[x]==LOW[x]){
tot++;
do{
color[sta[top]]=tot;//染色
instack[sta[top]]=0;
}while(sta[top--]!=x);
}
}
bool solve(){
for(int i=0;i<2*n;i++)
if(!DFN[i])
tarjan(i);
for(int i=0;i<2*n;i+=2)
if(color[i]==color[i+1])return 0;
return 1;
}
void init(){
top=0;
tot=0;
Index=0;
numedge=0;
memset(sta,0,sizeof(sta));
memset(DFN,0,sizeof(DFN));
memset(instack,0,sizeof(instack));
memset(LOW,0,sizeof(LOW));
memset(color,0,sizeof(color));
memset(head,0,sizeof(head));
}
int main(){
while(~scanf("%d%d", &n , &m )){
init();
for(int i = 1 ; i <= m ; i++){
int a1,a2,c1,c2;
scanf("%d%d%d%d",&a1,&a2,&c1,&c2);//自己做的时候别用cin会被卡
add( 2*a1+c1 , 2*a2+1-c2 );//我们将2i+1表示为第i对中的,2i表示为妻子。
add( 2*a2+c2 , 2*a1+1-c1 );
}
if( solve() )
printf("YES\n");
else
printf("NO\n");
}
return 0;
}
```
HDU1814 [和平委员会](http://acm.hdu.edu.cn/showproblem.php?pid=1814)
POJ3683 [牧师忙碌日](http://poj.org/problem?id=3683)