2-sat 學習筆記
1.前言:
這個蒟蒻自己都還沒學明白,所以講的不清楚一定要快評論下來,然後這樣蒟蒻思考了給大佬一點啟發。
2.正文:
\(2-sat\) 是什麼呢?
其實就是這樣的,\(2-sat\) 問題是形如: 有 \(n\) 個人,每個人可以是男的和女的,要滿足他們之間的一類關係,比如 \(i\) 和 \(j\) 中一個是男的,一個是女的。或者都是男的,或者都是女的。讓你對他們安排性別滿足條件。
用正式一點的話就是,有 \(n\) 個布林變數,每個變數可以為 \(0\) 或者 \(1\) 。讓你去安排每個變數是 \(0\) 還是 \(1\) ,去滿足約束條件。
當然有 \(2-sat\) 就有 \(k-sat\)
我們談談 \(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\)
那麼建圖是怎樣的呢?大概描述一下流程的話。就是:
- \(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}\)
- 對於每個不確定的變數 \(a_i\) ,令其為0,然後沿邊訪問相連的點。
- 檢查是否會導致任意一個 \(b\) 與 \(b+n\) 都被選中,如果不會那麼撤銷讓 \(a_i=0\)
- 否則讓 \(a_i=1\) 重複 \(2\) 操作。
- 繼續考慮不確定的限制
複雜度為 \(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\) 演算法,下面會更新一下例題輔助理解,期待!