图论 —— 2-SAT 问题
【問題概述】
2-SAT問題是這樣的:有n個(gè)布爾變量xi,另有m個(gè)需要滿足的條件,每個(gè)條件的形式都是“xi為真/假或者xj為真/假“
SAT 是適定性(Satisfiability)問題的簡(jiǎn)稱,一般形式為:k-適定性問題,簡(jiǎn)稱:k-SAT。
當(dāng) k>2 時(shí),k-SAT 是 NP 完全的,因此一般討論的是 k=2 的情況,即:2-SAT 問題。
關(guān)于 2-SAT 問題,簡(jiǎn)單的來說就是給出 n 個(gè)集合,每個(gè)集合中有兩個(gè)元素,然后從每個(gè)集合中選出一個(gè)元素,一共選 n 個(gè)兩兩不矛盾的元素,?顯然可能有多種選擇方案,一般題目只需要求任意輸出一種即可。
簡(jiǎn)單來說,就是給出一個(gè)由 n?個(gè)布爾值組成的序列 A,再給出 m 個(gè)限制關(guān)系,每個(gè)條件的形式都是 Xi 為真/假 或 Xj 為真/假(比如:A[x] AND A[y]=0、A[x] OR A[y] OR A[z]=1 等),來確定 A[0..n-1] 的值,使得其滿足所有限制關(guān)系,這樣的問題就是?SAT 問題,特別的,若每種限制關(guān)系中最多只對(duì)兩個(gè)元素進(jìn)行限制,則稱為 2-SAT 問題。
【基本原理】
由于在 2-SAT 問題中,最多只對(duì)兩個(gè)元素進(jìn)行限制,所以可能的限制關(guān)系共有 11 種:
- A[x]:A[x]
- NOT A[x]:非 A[x]
- A[x] AND A[y]:A[x] 與?A[y]
- A[x] OR A[y]:A[x] 或?A[y]
- A[x] XOR A[y]:A[x] 異或?A[y]
- A[x] AND NOT A[y]:A[x] 與 非A[y]
- A[x] OR NOT A[y]:A[x] 或 非A[y]
- A[x] XOR NOT A[y]:A[x] 異或 非A[y]
- NOT (A[x] AND A[y]):非(A[x] 與 A[y])
- NOT (A[x] OR A[y]):非(A[x] 或 A[y])
- NOT (A[x] XOR A[y]):非(A[x] 異或 A[y])
進(jìn)一步來說,A[x] AND A[y] 相當(dāng)于?(A[x]) AND (A[y]),也就是可以拆分成 A[x] 與 A[y] 兩個(gè)限制關(guān)系;NOT (A[x] OR A[y]) 相當(dāng)于 NOT A[x] AND NOT A[y],也就是可以拆分成 NOT A[x] 與 NOT A[y] 兩個(gè)限制關(guān)系。因此,可能的限制關(guān)系最多只有9種。
在實(shí)際問題中,2-SAT 問題大多數(shù)表現(xiàn)為以下形式:給出 n 對(duì)物品,每對(duì)物品必須選取一個(gè)且只能選一個(gè),而且給出它們之間存在的某些限制關(guān)系,如:某兩個(gè)物品不能都選、某兩個(gè)物品不能都不選等等,這時(shí)可以將每對(duì)物品當(dāng)成一個(gè)布爾值(選取第一個(gè)取 0,選取第二個(gè)取 1),如果所有與的限制關(guān)系最多只對(duì)兩個(gè)物品進(jìn)行限制,則它們都可以轉(zhuǎn)換成 9 種基本限制模型,從而轉(zhuǎn)換為 2-SAT 問題模型。
【問題解決】
以 A、B、C 三個(gè)人出去玩為例:如果 B 去,則 A 也去;B、C 只去一個(gè);C一定去
假設(shè)用 ' 表示某個(gè)點(diǎn)不選,那么根據(jù)上面的敘述有:
- B->A
- B'->C,C'->B
- C'->C
對(duì)于這種問題,我們可以用 Tarjan 來解決
首先將給出的 n 個(gè)變量的每個(gè)變量 i 拆分成兩個(gè)結(jié)點(diǎn) i、i+n,分別表示第 i 個(gè)變量為真、第 i+n 個(gè)變量為假,這樣就將 n 個(gè)點(diǎn)拆分成了 n 對(duì)。
然后根據(jù)題設(shè)所給出的 m 個(gè)關(guān)系,建立一個(gè) 2*n 的有向圖,即根據(jù)題設(shè)在圖中構(gòu)造有向邊:若圖中存在邊 <i,j>,則表示若選了 i 則必須選 j,可以發(fā)現(xiàn),上述的 9 種限制關(guān)系中,前 2 種一元關(guān)系可通過連一條邊來實(shí)現(xiàn),后 7 種二元關(guān)系可通過連兩條邊來實(shí)現(xiàn)。
對(duì)于一元關(guān)系:對(duì)于 Xi?為真,可以通過連邊 <i+n,i> 實(shí)現(xiàn),Xi?為假,可以通過連邊 <i,i+n> 來實(shí)現(xiàn)
對(duì)于二元關(guān)系:以?“Xi 為假或者 Xj 為假“ 為例,其可以表述為:若 Xi 為真,則 Xj 為假,若 Xj 為真,則 Xi 為假,因此需要連兩條邊:<i,j+n>、<j,i+n>
最后根據(jù)建出的圖跑一遍 Tarjan 來求出所有的強(qiáng)連通分量,然后根據(jù)拓?fù)湫騺頉Q定每個(gè)點(diǎn)是選還是不選,由于 Tarjan 給出的是反拓?fù)湫?#xff0c;因此只要找強(qiáng)連通分量編號(hào)小的即可。
關(guān)于拓?fù)湫?#xff1a;以 x->x' 為例,如果選了 x 那么 x' 也要選,但這條邊的意思是 x 這個(gè)點(diǎn)一定不選,于是就構(gòu)成矛盾,只能選擇拓?fù)湫虼蟮?/strong>
這樣在跑完 Tarjan 后,如果同一物品的兩種狀態(tài)在同一個(gè)邊雙連通分量中,則說明無解,若不在同一邊雙連通分量中,則可以輸出選擇方案,即每個(gè)點(diǎn)選擇縮成的超級(jí)點(diǎn)中編號(hào)最小的那個(gè)
【模版】
1.Tarjan
以以下問題為例:
有 n 個(gè)布爾變量 m 個(gè)需滿足的條件,每個(gè)條件的形式都是 " xi 為 true/false 或 xj 為 true/false ",2-SAT 問題的目標(biāo)是給每個(gè)變量賦值,使得所有條件滿足,現(xiàn)在給出 m 個(gè)條件,形式為:i a j b,表示 xi 為 a 或 xj 為 b,其中 a、b∈{0,1}
若無解,則輸出 IMPOSSIBLE,若有解,輸出 POSSIBLE 與構(gòu)造的 n 個(gè)變量的解
struct Edge{int to,next; }edge[N*2]; int head[N],tot; int n,m; int dfn[N],low[N]; bool vis[N];//標(biāo)記數(shù)組 int scc[N];//記錄結(jié)點(diǎn)i屬于哪個(gè)強(qiáng)連通分量 int block_cnt;//時(shí)間戳 int sig;//記錄強(qiáng)連通分量個(gè)數(shù) stack<int> S; void init(){tot=0;sig=0;block_cnt=0;memset(head,-1,sizeof(head));memset(vis,0,sizeof(vis));memset(dfn,0,sizeof(dfn));memset(low,0,sizeof(low));memset(scc,0,sizeof(scc)); } void addEdge(int from,int to){edge[++tot].to=to;edge[tot].next=head[from];head[from]=tot; } void Tarjan(int x) {vis[x]=true;dfn[x]=low[x]=++block_cnt;//每找到一個(gè)新點(diǎn),紀(jì)錄當(dāng)前節(jié)點(diǎn)的時(shí)間戳S.push(x);//當(dāng)前結(jié)點(diǎn)入棧for(int i=head[x]; i!=-1; i=edge[i].next) { //遍歷整個(gè)棧int y=edge[i].to;//當(dāng)前結(jié)點(diǎn)的下一結(jié)點(diǎn)if(!dfn[y]) {Tarjan(y);low[x]=min(low[x],low[y]);}else if(vis[y])low[x]=min(low[x],dfn[y]);}if(dfn[x]==low[x]) { //滿足強(qiáng)連通分量要求sig++;//記錄強(qiáng)連通分量個(gè)數(shù)while(true) { //記錄元素屬于第幾個(gè)強(qiáng)連通分量int temp=S.top();S.pop();vis[temp]=false;scc[temp]=sig;if(temp==x)break;}} } bool twoSAT(){for(int i=1;i<=2*n;i++)//找強(qiáng)連通分量if(!dfn[i])Tarjan(i);for(int i=1;i<=n;i++)if(scc[i]==scc[i+n])//條件a與!a屬于同一連通分量,無解return false;return true; } int main() {init();scanf("%d%d",&n,&m);while(m--) {int x,y,xVal,yVal;scanf("%d%d%d%d",&x,&xVal,&y,&yVal);if(xVal==0&&yVal==0){//x為0或y為0addEdge(x+n,y);//x為0,y為1addEdge(y+n,x);//y為0,x為1}else if(xVal==0&&yVal==1){//x為0或y為1addEdge(x+n,y+n);//x為0,y為0addEdge(y,x);//y為1,x為1}else if(xVal==1&&yVal==0){//x為1或y為0addEdge(x,y);//x為1,y為1addEdge(y+n,x+n);//y為0,x為0}else if(xVal==1&&yVal==1){//x為1或y為1addEdge(x,y+n);//x為1,y為0addEdge(y,x+n);//y為1,x為0}}bool flag=twoSAT();if(!flag)printf("IMPOSSIBLE\n");else{printf("POSSIBLE\n");for(int i=1;i<=n;i++){if(scc[i]>scc[i+n])printf("1 ");elseprintf("0 ");}printf("\n");}return 0; }2.多次 dfs
將初始的 n 個(gè)物品變成 2n 個(gè)節(jié)點(diǎn),然后從 0 開始編號(hào)到 2*n-1,其中原始第 i 個(gè)物品對(duì)應(yīng)節(jié)點(diǎn) i*2 和 i*2+1,如果標(biāo)記?vis[i*2] 節(jié)點(diǎn),那么表示 i 節(jié)點(diǎn)設(shè)為假,如果標(biāo)記 vis[i*2+1] 節(jié)點(diǎn),那么 i 節(jié)點(diǎn)設(shè)為真,同一個(gè)節(jié)點(diǎn)只能標(biāo)記一種結(jié)果,即對(duì)于原始 i 來說,只能標(biāo)記 vis[i*2] 或 vis[i*2+1] 其中之一
然后加入存在 i 假或 j 假的論述,引一條圖中從 2*i+1 到 2*j 的邊,再引一條 2*j+1 到 2*i 的邊,表示如果 i 是真的,那么 j 肯定是假的,且如果 j 是真的,那么 i 肯定是假的,否則之前的結(jié)論不成立
如果存在 i 為真的論述,那么直接標(biāo)記 vis[i*2+1]
最終判斷整個(gè)問題是否有解,就是做多次dfs來設(shè)置每個(gè)節(jié)點(diǎn)可能的值(真或假),看看是否所有可能取值情況都會(huì)沖突,如果不沖突,那么有解
vector<int> G[N*2];//G[i]==j表示如果vis[i]=true,那么vis[j]也要=true bool vis[N*2];//標(biāo)記數(shù)組 int Stack[N*2],tot;//用來記錄一次dfs遍歷的所有節(jié)點(diǎn)編號(hào) void init(int n) {for(int i=0; i<2*n; i++)G[i].clear();memset(vis,0,sizeof(vis)); } //加入(x,xval)或(y,yval)條件,xval=0表示假,yval=1表示真 void addEdge(int x,int xval,int y,int yval) {x=x*2+xval;y=y*2+yval;//添加雙向邊G[x^1].push_back(y);G[y^1].push_back(x); } bool dfs(int x) {//從x執(zhí)行dfs遍歷,途徑的所有點(diǎn)都標(biāo)記,如果不能標(biāo)記,那么返回falseif(vis[x^1])return false;if(vis[x])return true;vis[x]=true;Stack[tot++]=x;for(int i=0; i<G[x].size(); i++)if(!dfs(G[x][i]))return false;return true; } bool towSAT(int n) {//判斷當(dāng)前2-SAT問題是否有解for(int i=0; i<2*n; i+=2){if(!vis[i] && !vis[i+1]) {tot=0;if(!dfs(i)) {while(tot>0)vis[Stack[--tot]]=false;if(!dfs(i+1))return false;}}}return true; } int main(){int n,m;scanf("%d%d",&n,&m);init(n);while(m--){int a,b,aVal,bVal;scanf("%d%d%d%d",&a,&b,&aVal,&bVal);if(a==0&&b==0){addEdge(...);addEdge(...);}else if(a==0&&b==1){addEdge(...);addEdge(...);}else if(a==1&&b==0){addEdge(...);addEdge(...);}else if(a==1&&b==1){addEdge(...);addEdge(...);}}bool flag=twoSAT(n);if(flag)printf("YES\n");elseprintf("NO\n");return 0; }【例題】
總結(jié)
以上是生活随笔為你收集整理的图论 —— 2-SAT 问题的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 信息学奥赛一本通(1051:分段函数)
- 下一篇: 信息学奥赛一本通(1037:计算2的幂)