1. 程式人生 > >演算法學習筆記:2-SAT

演算法學習筆記:2-SAT

> SAT 是適定性(Satisfiability)問題的簡稱。一般形式為 k - 適定性問題,簡稱 k-SAT。而當 $k>2$ 時該問題為 NP 完全的。所以我們只研究 $k=2$ 的情況。 ## 定義 2-SAT,簡單的說就是給出 $n$ 個集合,每個集合有兩個元素,已知若干個 $$ ,表示 $a$ 與 $b$ 矛盾(其中 $a$ 與 $b$ 屬於不同的集合)。然後從每個集合選擇一個元素,判斷能否一共選 $n$ 個兩兩不矛盾的元素。顯然可能有多種選擇方案,一般題中只需要求出一種即可。 ## 現實意義 比如邀請人來吃喜酒,夫妻二人必須去一個,然而某些人之間有矛盾(比如 A 先生與 B 女士有矛盾,C 女士不想和 D 先生在一起),那麼我們要確定能否避免來人之間沒有矛盾,有時需要方案。這是一類生活中常見的問題。 使用布林方程表示上述問題。設 $a$ 表示 A 先生去參加,那麼 B 女士就不能參加( $\neg a$ ); $b$ 表示 C 女士參加,那麼 $\neg b$ 也一定成立(D 先生不參加)。總結一下,即 $(a \vee b)$ (變數 $a, b$ 至少滿足一個)。對這些變數關係建有向圖,則有: $\neg a\Rightarrow b\wedge\neg b\Rightarrow a$ ( $a$ 不成立則 $b$ 一定成立;同理, $b$ 不成立則 $a$ 一定成立)。建圖之後,我們就可以使用縮點演算法來求解 2-SAT 問題了。 ## 常用解決方法 ### Tarjan [SCC 縮點](https://www.cnblogs.com/RioTian/p/13444771.html) 演算法考究在建圖這點,我們舉個例子來講: 假設有 ${a1,a2}$ 和 ${b1,b2}$ 兩對,已知 $a1$ 和 $b2$ 間有矛盾,於是為了方案自洽,由於兩者中必須選一個,所以我們就要拉兩條條有向邊 $(a1,b1)$ 和 $(b2,a2)$ 表示選了 $a1$ 則必須選 $b1$ ,選了 $b2$ 則必須選 $a2$ 才能夠自洽。 然後通過這樣子建邊我們跑一遍 Tarjan SCC 判斷是否有一個集合中的兩個元素在同一個 SCC 中,若有則輸出不可能,否則輸出方案。構造方案只需要把幾個不矛盾的 SCC 拼起來就好了。 輸出方案時可以通過變數在圖中的拓撲序確定該變數的取值。如果變數 $\neg x$ 的拓撲序在 $x$ 之後,那麼取 $x$ 值為真。應用到 Tarjan 演算法的縮點,即 $x$ 所在 SCC 編號在 $\neg x$ 之前時,取 $x$ 為真。因為 Tarjan 演算法求強連通分量時使用了棧,所以 Tarjan 求得的 SCC 編號相當於反拓撲序。 顯然地,時間複雜度為 $O(n+m)$ 。 ### 爆搜 就是沿著圖上一條路徑,如果一個點被選擇了,那麼這條路徑以後的點都將被選擇,那麼,出現不可行的情況就是,存在一個集合中兩者都被選擇了。 那麼,我們只需要列舉一下就可以了,資料不大,答案總是可以出來的。 #### 爆搜模板 下方程式碼來自劉汝佳的白書: ```cpp // 來源:白書第 323 頁 struct Twosat { int n; vector g[maxn * 2]; bool mark[maxn * 2]; int s[maxn * 2], c; bool dfs(int x) { if (mark[x ^ 1]) return false; if (mark[x]) return true; mark[x] = true; s[c++] = x; for (int i = 0; i < (int)g[x].size(); i++) if (!dfs(g[x][i])) return false; return true; } void init(int n) { this->n = n; for (int i = 0; i < n * 2; i++) g[i].clear(); memset(mark, 0, sizeof(mark)); } void add_clause(int x, int y) { // 這個函式隨題意變化 g[x].push_back(y ^ 1); // 選了 x 就必須選 y^1 g[y].push_back(x ^ 1); } bool solve() { for (int i = 0; i < n * 2; i += 2) if (!mark[i] && !mark[i + 1]) { c = 0; if (!dfs(i)) { while (c > 0) mark[s[--c]] = false; if (!dfs(i + 1)) return false; } } return true; } }; ``` ## 例題 ### **HDU3062 [Party](http://acm.hdu.edu.cn/showproblem.php?pid=3062) ** > 題面:有 n 對夫妻被邀請參加一個聚會,因為場地的問題,每對夫妻中只有 $1$ 人可以列席。在 $2n$ 個人中,某些人之間有著很大的矛盾(當然夫妻之間是沒有矛盾的),有矛盾的 $2$ 個人是不會同時出現在聚會上的。有沒有可能會有 $n$ 個人同時列席? 這是一道多校題,裸的 2-SAT 判斷是否有方案,按照我們上面的分析,如果 $a1$ 中的丈夫和 $a2$ 中的妻子不合,我們就把 $a1$ 中的丈夫和 $a2$ 中的丈夫連邊,把 $a2$ 中的妻子和 $a1$ 中的妻子連邊,然後縮點染色判斷即可。 ```cpp #include
#define maxn 2018 #define maxm 4000400 using namespace std; int Index, instack[maxn], DFN[maxn], LOW[maxn]; int tot, color[maxn]; int numedge, head[maxn]; struct Edge { int nxt, to; } edge[maxm]; int sta[maxn], top; int n, m; void add(int x, int y) { edge[++numedge].to = y; edge[numedge].nxt = head[x]; head[x] = numedge; } void tarjan(int x) { // 縮點看不懂請移步強連通分量上面有一個連結可以點。 sta[++top] = x; instack[x] = 1; DFN[x] = LOW[x] = ++Index; for (int i = head[x]; i; i = edge[i].nxt) { int v = edge[i].to; if (!DFN[v]) { tarjan(v); LOW[x] = min(LOW[x], LOW[v]); } else if (instack[v]) LOW[x] = min(LOW[x], DFN[v]); } if (DFN[x] == LOW[x]) { tot++; do { color[sta[top]] = tot; // 染色 instack[sta[top]] = 0; } while (sta[top--] != x); } } bool solve() { for (int i = 0; i < 2 * n; i++) if (!DFN[i]) tarjan(i); for (int i = 0; i < 2 * n; i += 2) if (color[i] == color[i + 1]) return 0; return 1; } void init() { top = 0; tot = 0; Index = 0; numedge = 0; memset(sta, 0, sizeof(sta)); memset(DFN, 0, sizeof(DFN)); memset(instack, 0, sizeof(instack)); memset(LOW, 0, sizeof(LOW)); memset(color, 0, sizeof(color)); memset(head, 0, sizeof(head)); } int main() { while (~scanf("%d%d", &n, &m)) { init(); for (int i = 1; i <= m; i++) { int a1, a2, c1, c2; scanf("%d%d%d%d", &a1, &a2, &c1, &c2); // 自己做的時候別用 cin 會被卡 add(2 * a1 + c1, 2 * a2 + 1 - c2); // 我們將 2i+1 表示為第 i 對中的,2i 表示為妻子。 add(2 * a2 + c2, 2 * a1 + 1 - c1); } if (solve()) printf("YES\n"); else printf("NO\n"); } return 0; } ``` ## 練習題 HDU1814 [和平委員會](http://acm.hdu.edu.cn/showproblem.php?pid=1814) POJ3683 [牧師忙碌日](http://poj.org/problem?id=3683) ## 其它 文章開源在 [Github - blog-articles](https://github.com/RivTian/blog-articles),點選 Watch 即可訂閱本部落格。 若文章有錯誤,請在 [Issues](https://github.com/RivTian/blog-articles/issues) 中提出,我會及時回覆,謝謝。 如果您覺得文章不錯,或者在生活和工作中幫助到了您,不妨給個 Star,謝謝。 (