1. 程式人生 > 其它 >2-sat 學習筆記

2-sat 學習筆記

1.前言:

這個蒟蒻自己都還沒學明白,所以講的不清楚一定要快評論下來,然後這樣蒟蒻思考了給大佬一點啟發。

2.正文:

\(2-sat\) 是什麼呢?

其實就是這樣的,\(2-sat\) 問題是形如: 有 \(n\) 個人,每個人可以是男的和女的,要滿足他們之間的一類關係,比如 \(i\)\(j\) 中一個是男的,一個是女的。或者都是男的,或者都是女的。讓你對他們安排性別滿足條件。

用正式一點的話就是,有 \(n\) 個布林變數,每個變數可以為 \(0\) 或者 \(1\) 。讓你去安排每個變數是 \(0\) 還是 \(1\) ,去滿足約束條件。

當然有 \(2-sat\) 就有 \(k-sat\)

等,但是目前 \(k > 2\) 是多項式時間不可解的問題。

我們談談 \(2-sat\) 的問題是怎麼解決的。

這裡我們先引入一道例題

我們發現這道題裡面,是一個很裸的 \(2-sat\) ,我們思考每個變數之間的關係。

我們首先發現的是每一個點,只有 \(2\) 個變數取值,我們考慮把每個變數 \(i\) 拆成兩個點 \(i\)\(i+n\) 。表示兩個點是 \(\text{true}\) 還是 \(\text{false}\)

然後我們去考慮對於每一條限制怎麼做。

如果有 \(a\) & \(b=0\)

這個表明了 \(a\)\(b\) 裡面一定有一個是 \(0\)

那麼我們要在 \(a\)\(true\) 裡 連邊向 \(b\)\(false\) 。並且將 \(b\)\(true\) 連向 \(a\)\(false\)

這樣可以保證的是無論這兩個的取值是怎樣,反正最終會有一個走向 \(false\) 。所以這個建邊是沒問題的,這裡應該沒問題。

但是,有問題就是我們為什麼不能去,從 \(b\)\(false\) 連向 \(a\)\(true\)

這麼乍一看也沒什麼問題啊?

但是這樣不行的,因為如果 \(b\)\(false\) 其實 \(a\)\(true\) 還是 \(false\) 都是沒問題的。而這麼連邊就是不明確的關係,讓人認為,選了\(b\)

\(false\)\(a\) 就只能為 \(true\) 了,所以不行。

那麼建圖是怎樣的呢?大概描述一下流程的話。就是:

  • \(a,b\) 不能同時選,那麼選了 \(a\) 就要選 \(b+n\) ,選了 \(b\) 就要選 \(a+n\)\(a\rightarrow b+n\)\(b \rightarrow a+n\)
  • \(a,b\) 必須同時選,那麼選了 \(a\) 就要選 \(b\) ,選了 \(b\) 就要選 \(a\) 。則 \(a\rightarrow b\)\(b \rightarrow a\)
  • \(a,b\) 中至少選一個,那麼選了 \(a\) 就要選 \(b+n\) ,選 \(b\) 就要選 \(a+n\) ,選了 \(a+n\) 就要選 \(b\) ,選了 \(b+n\) 就要選 \(a\) 。則 \(a\rightarrow b+n\)\(b \rightarrow a+n\)\(a+n \rightarrow b\)\(b+n \rightarrow a\)
  • \(a,b\)\(a\) 必須選,那麼讓 \(a+n \rightarrow a\)

然後就可以了完成建圖過程,但是我們還需要知道是否存在分派方案和怎麼分配。

怎麼去判斷是否有解,在 \(\text{hl666}\) 神仙的部落格裡學到了兩種方法。

第一種:\(\text{DFS}\)
  1. 對於每個不確定的變數 \(a_i\) ,令其為0,然後沿邊訪問相連的點。
  2. 檢查是否會導致任意一個 \(b\)\(b+n\) 都被選中,如果不會那麼撤銷讓 \(a_i=0\)
  3. 否則讓 \(a_i=1\) 重複 \(2\) 操作。
  4. 繼續考慮不確定的限制

複雜度為 \(n^2+nm\) ,可以用 \(bitset\) 優化傳遞閉包後進行 \(\dfrac{n^3}{w}\) 的預處理做到 \(n+m\) 的 DFS 。

這樣做可以保證求解出的答案字典序最小。

第二種:\(\text{Tarjan}\)

無解的情況其實就是某一變數的 \(false\) 走到 \(true\) 而從 \(true\) 也能走到 \(false\) 也就是一個變數的取值在同一強連通分量內就無解。

而且分析一下,你還可以知道,在同一強連通分量內的變數取值相同。

怎麼去求解取值呢?

我們要選的其實就是在圖中被邊指著的變數值,這樣才可以保證不會產生矛盾。

那麼在拓撲序上就是我們要選擇拓撲序上較大的值。

因為在有向圖中拓撲排序後,被指向的點的拓撲序大於指向它的點。

但是其實在求解 \(\text{Tarjan}\) 時已經求解除了拓撲序,只不過時反向的,可以思考 $\text{Tarjan} $ 的過程理解。

然後取兩個取值中強連通分量的編號較小的所對應的值就可以了。

時間複雜度為 \(n+m\)

下面給出例題程式碼的參考實現:


#include<bits/stdc++.h>
using namespace std;
#define MAXN 900
struct ios_in{
 inline char gc(){
  static char buf[MAXN],*l,*r;
  return (l==r)&&(r=(l=buf)+fread(buf,1,MAXN,stdin),l==r)?EOF:*l++;
 }
 template <typename _Tp>
 inline ios_in&operator>>(_Tp&x){
  static char ch,sgn;
  for(sgn=0,ch=gc();!isdigit(ch);ch=gc()) {
   if(!~ch) return *this;
   sgn|=ch=='-';
  }
  for(x=0;isdigit(ch);ch=gc()) x=(x<<1)+(x<<3)+(ch^'0');
   sgn&&(x=-x);
   return *this;
 }
}Cin;
const int N = 3e6;
int n,m,x[N];
int dfn[N],low[N],cnt,nex[N],first[N],v[N],num;
int ins[N],tp,sc,scc[N],sz[N],s[N];
//scc[i]表示i所在scc的編號
void add(int from,int to){nex[++num]=first[from];first[from]=num;v[num]=to;	}
void tarjan(int u){
 low[u]=dfn[u]=++cnt;s[++tp]=u;ins[u]=1;
 for(int i=first[u];i;i=nex[i]){
  int to=v[i];
  if(!dfn[to]){
   tarjan(to);
   low[u]=min(low[u],low[to]);	
  }
  else if(ins[to]){
   low[u]=min(low[u],dfn[to]);	
  }	
 }
 if(dfn[u]==low[u]){
  ++sc;
  while(s[tp]!=u){
   scc[s[tp]]=sc;
   sz[sc]++;
   ins[s[tp]]=0;
   --tp;	
  }
  scc[s[tp]]=sc;
  sz[sc]++;
  ins[s[tp]]=0;
  --tp;	
 }	
} 
signed main(){
 Cin>>n>>m;
 for(int i=1;i<=m;i++){
  int a,b,c,d;
  Cin>>a>>c>>b>>d;
  if(c&&d){
   add(a,b+n);
   add(b,a+n);	
  }
  if(!c&&!d){
   add(a+n,b);
   add(b+n,a);	
  }
  if(c&&!d){
   add(a,b);
   add(b+n,a+n);	
  }
  if(!c&&d){
   add(a+n,b+n);
   add(b,a);	
  }	
 }
 for(int i=1;i<=2*n;i++)
  if(!dfn[i]) tarjan(i);	
 
 for(int i=1;i<=n;i++)
  if(scc[i]==scc[i+n]){printf("IMPOSSIBLE");return 0;}	
 
 printf("POSSIBLE\n");
 for(int i=1;i<=n;i++){
  if(scc[i]>scc[i+n]){//強連通分量小,拓撲序大,答案優秀 
   printf("1 "); 	
  }
  else printf("0 ");	
 }
 return 0;	
}

下面有思路一樣的雙倍經驗

不過要注意對字串後面數字的處理要處理完,它不止一位。

到此就介紹完了 \(2-sat\) 演算法,下面會更新一下例題輔助理解,期待!

3.參考資料:

2-SAT超入門講解

2-SAT總結

2-sat研究