日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

图论 —— 2-SAT 问题

發(fā)布時間:2025/3/17 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 图论 —— 2-SAT 问题 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

【問題概述】

2-SAT問題是這樣的:有n個布爾變量xi,另有m個需要滿足的條件,每個條件的形式都是“xi為真/假或者xj為真/假“

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

當(dāng) k>2 時,k-SAT 是 NP 完全的,因此一般討論的是 k=2 的情況,即:2-SAT 問題。

關(guān)于 2-SAT 問題,簡單的來說就是給出 n 個集合,每個集合中有兩個元素,然后從每個集合中選出一個元素,一共選 n 個兩兩不矛盾的元素,?顯然可能有多種選擇方案,一般題目只需要求任意輸出一種即可。

簡單來說,就是給出一個由 n?個布爾值組成的序列 A,再給出 m 個限制關(guān)系,每個條件的形式都是 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)系中最多只對兩個元素進(jìn)行限制,則稱為 2-SAT 問題。

【基本原理】

由于在 2-SAT 問題中,最多只對兩個元素進(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] 兩個限制關(guān)系;NOT (A[x] OR A[y]) 相當(dāng)于 NOT A[x] AND NOT A[y],也就是可以拆分成 NOT A[x] 與 NOT A[y] 兩個限制關(guān)系。因此,可能的限制關(guān)系最多只有9種。

在實(shí)際問題中,2-SAT 問題大多數(shù)表現(xiàn)為以下形式:給出 n 對物品,每對物品必須選取一個且只能選一個,而且給出它們之間存在的某些限制關(guān)系,如:某兩個物品不能都選、某兩個物品不能都不選等等,這時可以將每對物品當(dāng)成一個布爾值(選取第一個取 0,選取第二個取 1),如果所有與的限制關(guān)系最多只對兩個物品進(jìn)行限制,則它們都可以轉(zhuǎn)換成 9 種基本限制模型,從而轉(zhuǎn)換為 2-SAT 問題模型。

【問題解決】

以 A、B、C 三個人出去玩為例:如果 B 去,則 A 也去;B、C 只去一個;C一定去

假設(shè)用 ' 表示某個點(diǎn)不選,那么根據(jù)上面的敘述有:

  • B->A
  • B'->C,C'->B
  • C'->C

對于這種問題,我們可以用 Tarjan 來解決

首先將給出的 n 個變量的每個變量 i 拆分成兩個結(jié)點(diǎn) i、i+n,分別表示第 i 個變量為真、第 i+n 個變量為假,這樣就將 n 個點(diǎn)拆分成了 n 對。

然后根據(jù)題設(shè)所給出的 m 個關(guān)系,建立一個 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)。

對于一元關(guān)系:對于 Xi?為真,可以通過連邊 <i+n,i> 實(shí)現(xiàn),Xi?為假,可以通過連邊 <i,i+n> 來實(shí)現(xiàn)

對于二元關(guān)系:以?“Xi 為假或者 Xj 為假“ 為例,其可以表述為:若 Xi 為真,則 Xj 為假,若 Xj 為真,則 Xi 為假,因此需要連兩條邊:<i,j+n>、<j,i+n>

最后根據(jù)建出的圖跑一遍 Tarjan 來求出所有的強(qiáng)連通分量,然后根據(jù)拓?fù)湫騺頉Q定每個點(diǎn)是選還是不選,由于 Tarjan 給出的是反拓?fù)湫?#xff0c;因此只要找強(qiáng)連通分量編號小的即可。

關(guān)于拓?fù)湫?#xff1a;以 x->x' 為例,如果選了 x 那么 x' 也要選,但這條邊的意思是 x 這個點(diǎn)一定不選,于是就構(gòu)成矛盾,只能選擇拓?fù)湫虼蟮?/strong>

這樣在跑完 Tarjan 后,如果同一物品的兩種狀態(tài)在同一個邊雙連通分量中,則說明無解,若不在同一邊雙連通分量中,則可以輸出選擇方案,即每個點(diǎn)選擇縮成的超級點(diǎn)中編號最小的那個

【模版】

1.Tarjan

以以下問題為例:

有 n 個布爾變量 m 個需滿足的條件,每個條件的形式都是 " xi 為 true/false 或 xj 為 true/false ",2-SAT 問題的目標(biāo)是給每個變量賦值,使得所有條件滿足,現(xiàn)在給出 m 個條件,形式為:i a j b,表示 xi 為 a 或 xj 為 b,其中 a、b∈{0,1}

若無解,則輸出 IMPOSSIBLE,若有解,輸出 POSSIBLE 與構(gòu)造的 n 個變量的解

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屬于哪個強(qiáng)連通分量 int block_cnt;//時間戳 int sig;//記錄強(qiáng)連通分量個數(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;//每找到一個新點(diǎn),紀(jì)錄當(dāng)前節(jié)點(diǎn)的時間戳S.push(x);//當(dāng)前結(jié)點(diǎn)入棧for(int i=head[x]; i!=-1; i=edge[i].next) { //遍歷整個棧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)連通分量個數(shù)while(true) { //記錄元素屬于第幾個強(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 個物品變成 2n 個節(jié)點(diǎn),然后從 0 開始編號到 2*n-1,其中原始第 i 個物品對應(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è)為真,同一個節(jié)點(diǎn)只能標(biāo)記一種結(jié)果,即對于原始 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]

最終判斷整個問題是否有解,就是做多次dfs來設(shè)置每個節(jié)點(diǎn)可能的值(真或假),看看是否所有可能取值情況都會沖突,如果不沖突,那么有解

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)編號 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; }

【例題】

  • Let's go home(HDU-1824)(多次dfs):點(diǎn)擊這里
  • Wedding(POJ-3648)(多次dfs):點(diǎn)擊這里
  • Perfect Election(POJ-3905)(多次dfs):點(diǎn)擊這里
  • Katu Puzzle(POJ-3678)(多次dfs):點(diǎn)擊這里
  • Go Deeper(HDU-3715)(多次dfs+二分):點(diǎn)擊這里
  • Building roads(POJ-2749)(多次dfs+二分):點(diǎn)擊這里
  • Ikki's Story IV - Panda's Trick(POJ-3207)(多次dfs+范圍判斷):點(diǎn)擊這里
  • Priest John's Busiest Day(POJ-3683)(多次dfs+或關(guān)系的應(yīng)用):點(diǎn)擊這里
  • Eliminate the Conflict(HDU-4115)(多次dfs+三元且關(guān)系):點(diǎn)擊這里
  • Get Luffy Out(POJ-2723)(多次dfs+且關(guān)系的枚舉):點(diǎn)擊這里
  • Peaceful Commission(HDU-1814)(多次dfs+最小字典序):點(diǎn)擊這里
  • 2-SAT 問題(洛谷-P4782)(Tarjan):點(diǎn)擊這里
  • Party(HDU-3062)(Tarjan):點(diǎn)擊這里
  • 滿漢全席(洛谷-P4171)(Tarjan):點(diǎn)擊這里
  • 處女座與寶藏(2019牛客寒假算法基礎(chǔ)集訓(xùn)營 Day2-F)(Tarjan):點(diǎn)擊這里
  • 總結(jié)

    以上是生活随笔為你收集整理的图论 —— 2-SAT 问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。