集合枚舉子集-學習筆記
算法
有一個集合,請輸出它的所有子集。
子集,即為被這個這個集合包括的所有集合,包括空集。那么顯然,假如有 \(n\) 個元素,那么有 \(2^n\) 個子集。如何枚舉子集呢?
首先有一個顯然的方法:用 \(2^n\) 的 dfs 枚舉。但這樣有弊端:時空較大,而且比較麻煩。比如一個動態規划,你每次轉移都要跑一遍 dfs ,時空開銷很大,而且處理不方便。那么就有另外一種方法出現:循環枚舉。
首先放代碼:
for(int j=st;j;j=(j-1)&st) j2=st^j;
st
就是要枚舉的集合,j
就是子集,j2
就是 j
相對於 st
的補集。這里我們認為集合是一個二進制數中所有元素 \(1\) ,一個 \(1\) 代表一個元素。這樣為什么是正確的呢?
首先,為了保證枚舉出來的元素都是原式的子集,我們用了 &st
這個位運算操作,因此可以保證。
其次,因為減法和按位與都是不增操作,因此可以保證不重。
那如何保證不漏呢?
模擬一遍過程:st
=1101
,我們按照循環順序模擬過程。
\(【1】1101\)
\(【2】1100\)
\(【3】1001\)
\(【4】1000\)
\(【5】0101\)
\(【6】0100\)
\(【7】0001\)
我們假定 \(0000\) 也在元素中,然后按照一定規律排列子集:
有沒有發現規律?每一行,后三位都是相同的。所以這個算法的本質算是,對於每一個 \(1\) ,先把這一位設定為 \(1\) 枚舉后面所有位,再把這一位設定為 \(0\) 枚舉一遍,然后再往前面遞歸。因為減 \(1\) 可以看做把最后一個 \(1\) 設定為 \(0\) ,后面所有設定為 \(1\)。這里建議還是自己手玩一下,找一找規律。所以這個算法類似於二進制枚舉,考慮到了所有情況。
這個做法是從大到小枚舉子集,着實十分優美。但要注意的是這種辦法沒法枚舉空集,需要手動計算。
枚舉子集在一些位運算的題目中可能會涉及,而這種方法可以很好地減少代碼復雜度以及時空復雜度,同時也是一種很好的思想。
復雜度證明
空間不管。
時間的話,如果一個集合元素個數為 \(x\) ,復雜度為 \(O(2^x)\)。
有一種特殊情況:枚舉 \(0\) 到 \(2^n-1\) 的所有數的所有子集,此時復雜度即為:
相當於從 \(n\) 個位上選出 \(k\) 個位有值,然后有 \(C_n^k\) 種方案,每種方案 \(2^k\) 。
根據二項式定理:
可得:原式等價於:
等價於 \(3^n\) 。復雜度還算是比較優秀的。