1. 程式人生 > 其它 >關於狀壓DP列舉子集的方法與理解

關於狀壓DP列舉子集的方法與理解

我們現在要列舉狀壓集合 \(S\) 的子集,程式碼實現:

for (int S1=S;S1!=0;S1=(S1-1)&S) {
  S2=S^S1;
}

其中 \(S_1\) 就是我們列舉得到的子集,\(S_2\) 是當前子集 \(S_1\)\(S\) 內的補集,即 \(S_1 \bigoplus S_2 = S\)

\[{\because S_2 = S \bigoplus S_1} \] \[{\therefore S_2 \bigoplus S_1 = S \bigoplus S_1 \bigoplus S_1} \] \[{\therefore S_2 \bigoplus S_1 = S \bigoplus (S_1 \bigoplus S_1)} \] \[{\therefore S_2 \bigoplus S_1 = S \bigoplus 0} \] \[{\therefore S_2 \bigoplus S_1 = S} \]

贅述如下

現在來講一講為什麼是這樣的一個列舉方法,先讓我們來舉一個例子來模擬一下。

假設我們當前要列舉的是 \((10110)_2\) 的子集(子集仍然用 \(S_1\) 表示):

\(S_1 = (10110)_2 \Rightarrow (10100)_2 \Rightarrow (10010)_2 \Rightarrow (10000)_2 \Rightarrow (110)_2 \Rightarrow (100)_2 \Rightarrow (10)_2\)

根據例子,我們發現按照上面程式碼得到的結果是正確的,並且是把子集按照從大到小的順序枚舉出來的。那麼接下來我們來談談這樣列舉的正確性。

首先,一個集合它自己本身也是自己的一個集合,所以我們從這個集合本身開始列舉。

既然是列舉,那我們就先考慮把當前列舉得到的子集先 \(-1\)(即 \(S_1 - 1\)),但是這樣做不能保證 \(-1\) 後得到的狀態是原狀態的子集,但是我們注意到:根據與運算&的性質,我們不難發現如果兩個數 a, b,a < b,我們對這兩個數進行&運算,最後的結果一定是b的子集,因為我們與運算&得到的結果,其二進位制中所有1出現的位置在b的二進位制對應位置也是1。

現在已經說明了這樣做確實得到了原集合的一個子集 \(S_1\),但是還沒有說明我們通過上述方式列舉,可以列舉完原集合 \(S\) 的所有子集。

其實列舉子集就相當於在原集合的二進位制狀態下把一些1換為0,而我們每次 \(-1\)

然後進行與運算其實就是在把當前子集 \(S_1\) 的最右邊的1的右邊全部變為1,自己變為0,然後和集合 \(S\) 進行與運算把新增的1中不該出現的抹去,最後只剩下了原集合中存在的1了。

Preference