淺談2-SAT

xzj213發表於2020-08-15

什麼是2-SAT

2-sat問題是一個邏輯互斥問題,與我們小時候做過的一道數學題一樣。

A,B,C三個人中有兩個女生,其中如果A為女生,B一定不是女生,而且A與C性別相同,求A,B,C的性別。

這是一道非常簡單的題目,我們可以簡單分析一下。

我們用\(f[i]=0\)表示\(i\)為女生,\(f[i]=1\)表示\(i\)為男生,那麼“如果A為女生,B一定不是女生”這句話可以表示為\(if (f[1]==0) f[2]=1;\)反過來也一樣,即若B為女生,A一定不是。同理,“A與C性別相同”這句話就可以表示為\(f[3]=f[1]\)。而這,就是一道最簡單的2-sat問題。

那麼在此基礎上,我們就可以開始分析2-sat了。

概念

SAT是適定性(Satisfiability)問題的簡稱 。一般形式為k-適定性問題,簡稱 K-SAT。

可以證明,當\(K>2\)時,K-SAT是NP完全的。因此一般討論的是k=2的情況,即2-SAT問題。

我們通俗的說,就是給你n個變數\(a_i\),每個變數能且只能取0/1的值。同時給出若干條件,形式諸如\((not)a_i\) opt \((not)a_j = 0/1\),其中opt表示\(and,or,xor\)中的一種

而求解2-SAT的解就是求出滿足所有限制的一組a

如何求解2-sat

我們發現,每一個條件如上題中“如果A為女生,B一定不是女生”我們可以考慮將A與B拆點,拆為A為女(\(a_0\))、A為男(\(a_1\))、B為女(\(a_2\))和B為男(\(a_3\))。那麼這個條件我們就可以將\(a_1\)\(a_4\)連一條單向邊,\(a_3\)\(a_2\)連一條單向邊。那麼當我們發現它成為了環時,準確來說是當\(a_i\)\(a_{i+1}\)在同一個環中,也就是說某人(hzr)既要當男生又要當女生時,顯然是不成立的,此時是無解的。若沒有出現這種情況,就說明存在這樣一組解。

我們可以舉一些簡單的例子來總結下連邊的規律(用i′表示i的反面):

\(i,j\)不能同時選:選了\(i\)就要選\(j′\),選\(j\)就要選\(i′\)。故\(i \rightarrow j′\),\(j \rightarrow i′\)。一般操作即為\(a_i\) ^ \(a_j=1\)

\(i,j\)必須同時選:選了\(i\)就要選\(j\),選\(j\)就要選\(i\)。故\(i \rightarrow j\),\(j \rightarrow i\)。一般操作即為\(a_i\) ^ \(a_j\)=0

\(i,j\)任選(但至少選一個)選一個:選了\(i\)就要選\(j′\),選j就要選\(i′\),選\(i′\)就要選\(j\),選\(j′\)就要選\(i\)。故\(i \rightarrow j′\),\(j \rightarrow i′\),\(i′ \rightarrow j\),\(j′ \rightarrow i\)。一般操作即為\(a_i\) ^ \(a_j\)=1

\(i\)必須選:直接\(i′\)\(i\),可以保證無論怎樣都選\(i\)。一般操作為給出的\(a_i=1\)\(a_i\) & \(a_j=1\)

解法1——DFS

  • 對於每個當前不確定的變數\(a_i\),令\(a_i=0\)然後沿著邊DFS訪問相連的點。
  • 檢查如果會導致任意一個\(j\)\(j′\)都被選,那麼撤銷。否則令a_i=0
  • 否則令\(a_i=1\),重複2。如果還不行就無解。
  • 繼續考慮下一個不確定的變數

這裡貼一下DFS的板子

#include<bits/stdc++.h>
using namespace std;
const int maxn=4e6+5;
struct edge{
	int nex,to;
}e[maxn];
int fl[maxn],n,m,beg[maxn],tot,s[maxn],cnt;
int read() {
	int x=0,f=1;
	char ch=getchar();
	while(ch<'0' || ch>'9') {if(ch=='-')f=-f;ch=getchar();}
	while(ch>='0' && ch<='9') {x=x*10+ch-'0';ch=getchar();}
	return x*f;
}
void add(int x,int y) {
	e[++tot]=(edge){beg[x],y};
	beg[x]=tot;
}
bool dfs(int now) {
	if (fl[now^1]) return true;
	if (fl[now]) return false;
	s[++cnt]=now;fl[now]=1;
	for (int i=beg[now];i;i=e[i].nex) {
		int nex=e[i].to;
		if (dfs(nex)) return true;
	}
	return false;
}
int main() {
	n=read();m=read();
	for (int i=1;i<=m;i++) {
		int x=read()-1,u=read(),y=read()-1,v=read();
		add(x*2+1-u,y*2+v);
		add(y*2+1-v,x*2+u);
	}
	for (int i=0;i<n*2;i+=2) {
		cnt=0;
		if (dfs(i)) {
			while(cnt) fl[s[cnt--]]=0;
			if (dfs(i+1)) {
				puts("IMPOSSIBLE");
				return 0;
			}
		}
	}
	puts("POSSIBLE");
	for (int i=0;i<n;i++) {
		if (fl[i*2]) cout<<0<<' ';
		else cout<<1<<' ';
	}
	return 0;
}
//複雜度(O(n+m))

解法2——SCC

考慮我們上面的判斷有無解的情況,我們想到完全可以藉助SCC來判斷兩個點是否互相到達。

那麼我們先縮點,如果\(i\)\(i′\)在同一SCC裡那麼顯然無解。

否則選擇i與i′中拓撲序較大的一個就可以得到一組可行解。

不是很常用(主要是一般題目的資料範圍都不需要),用來判可行性比較好。

例題

P4782 【模板】2-SAT 問題
HDU 3062Party
[NOI2017]遊戲