列舉所有子集合(enumerating all subsets)
你的架上有 n 件物品,想把每一種可能的選法都考慮一遍:一件都不拿、全拿、只拿奇數位的、隨意混搭。所有這些選法的全體叫做冪集合,它恰有 2^n 個成員,因為每件物品都各自面對一個要或不要的決定——納入或不納入。把它們全部列出、每個一次,就是列舉所有子集合。
看清這件事最乾淨的方式是位元遮罩技巧。把物品編號 0 到 n-1。任一子集合對應一個 n 位元的二進位數,若物品 i 被納入則第 i 位為 1。於是子集合與整數 0, 1, 2, ..., 2^n - 1 一一對應:只要從 0 數到 2^n - 1,對每個整數遮罩讀出它的位元,就還原出子集合。對 n = 3,遮罩 000、001、010、011、100、101、110、111 給出空集、{0}、{1}、{0,1}、{2}、{0,2}、{1,2}、{0,1,2}。這自動就是完備且無重複的,因為 0 到 2^n - 1 的整數每個恰被碰到一次。另一種看法是一棵遞迴決策樹,在每件物品上分岔兩次(略過/拿取),它的 2^n 個葉子就是那些子集合——同一套列舉,只是看成深度優先搜尋。
子集合列舉是 0/1 背包、子集合加總、暴力法做集合覆蓋,以及任何被表述為「選一個滿足某性質的子集合」之問題的主力。生成一個子集合花的時間與 n 成正比(讀它的位元或複製選中的物品),所以總工作量是 Theta(n * 2^n)——n 到約 20 至 25 還行,再大就無望。當 n 更大時,你不會去列舉所有子集合;你改用動態規劃、折半相遇,或剪枝。
用數遮罩 0..7 來列舉 {a, b, c} 的子集合:000 -> {}、001 -> {a}、010 -> {b}、011 -> {a,b}、100 -> {c}、101 -> {a,c}、110 -> {b,c}、111 -> {a,b,c}。8 = 2^3 個子集合每個恰出現一次,順序固定且可重現。
從 0 數到 2^n - 1,就是一套完備、無重複的全子集合列舉——整個訣竅就這麼簡單。
2^n 這個數是精確值而非上界,所以對「真正完整列舉」而言這代價無可迴避;若你發現自己對幾十的 n 需要所有子集合,那是該換演算法的訊號,而不是去最佳化那個迴圈。