前言
今天整理以前的競賽筆記時,發現了當時寫的一個模板:
列舉{0,1,…,n-1}所包含的所有大小為k的子集:
int comb = (1 << k) - 1; while (comb < 1 << n) { //進行鍼對組合的處理 int x = comb & -comb, y = comb + x; comb = ((comb&~y) / x >> 1) | y; }
我愣是看了半天,也沒想明白當時我想表達什麼(lll¬ω¬)
然後就百度了一下,結合一些描述,終於想起來這貌似是從小白書上扒下來的
話說我小白書已經失蹤一年了,到現在還沒找到......
以防以後又把它忘了,特此記錄
什麼是“列舉{0,1,…,n-1}所包含的所有大小為k的子集”
“列舉{0,1,…,n-1}所包含的所有大小為k的子集”與二進位制狀態壓縮關係密切,其本質為利用二進位制與位元算表示和操作集合,舉個例子:
含有n個元素的集合{0,1,…,n-1},就有n個二進位制位,第i
個二進位制位代表第i
個元素,第i
個二進位制位為1代表第i
個元素存在於集合,第i
位二進位制位為0代表第i
個元素不存在於集合。(i
<=n)
含有3個元素的集合{0,1,2}
,全部子集有0000
、0001
、0010
、0011
、0100
、0101
、0110
、0111
,其中0000
代表空集∅
。
二進位制數與集合對應關係如下:
我們不難得出,列舉集合{0,1,…,n-1}的所有子集的方法:
for (int S = 0; S < 1 << n; S++) {
//對子集的操作
}
S < 1 << n
等同於S <= ( (1<<n)-1 )
,(1<<n)-1
為含有n個元素的集合{0,1,…,n-1}。
解決“列舉{0,1,…,n-1}所包含的所有大小為k的子集”,我們只需弄清什麼是“大小為k的子集”。
“大小為k的子集”就是有k個元素的子集,也就是二進位制中有k個1。
含有3個元素的集合{0,1,2}
所包含的所有大小為2
的子集:
如何列舉?
為了將所有情況列舉出來,我們可以列舉集合{0,1,…,n-1}的所有子集,在列舉時加入判斷,判斷當前子集是否滿足“大小為k的子集”。
從實現上來看,這是可行的:
int n, k;
int getsum(int S) {// 統計二進位制中1的個數
int ans = 0;
while (S){
if (S & 1)
ans++;
S >>= 1;
}
return ans;
}
for (int S = 0; S < 1 << n; S++) {
if (getsum(S) == k) {
// 對子集的操作
}
}
但這不夠優秀,不如說相當低效,這時我們需要找到一種更優秀的列舉方法。
白書上提供了一種思路:
int comb = (1 << k) - 1;
while (comb < 1 << n) {
//進行鍼對組合的處理
int x = comb & -comb, y = comb + x;
comb = ((comb&~y) / x >> 1) | y;
}
comb
是按字典序排列的最小子集,在while
迴圈中,comb
會一直增大,直到找完所有大小為k的子集。
我們利用剛剛的例子,來模擬演算法找“含有3個元素的集合{0,1,2}
所包含的所有大小為2
的子集”的過程:
(此例中 k=2,n=3)
第一次迴圈,我們找到了按字典序排列的最小子集,也就是comb
的初始值0011
,之後comb
“按演算法提供方法”增大,comb
的值變為0101
。
第二次迴圈,我們找到的是0101
,之後comb
“按演算法提供方法”增大,comb
的值變為0110
。
第三次迴圈,我們找到的是0110
,之後comb
“按演算法提供方法”增大,comb
的值變為1001
。
此時,comb
的值不滿足“comb
< 1<<n
”即不滿足"1001
< 1000
",演算法結束於第四次迴圈的開始。
“按演算法提供方法”也就是每次求下一個子集的方法如下:
(以1100 1100
到其下一個子集1101 0001
為例)
int comb = (1 << k) - 1; while (comb < 1 << n) { //進行鍼對組合的處理 int x = comb & -comb, y = comb + x; comb = ((comb&~y) / x >> 1) | y; }
我們將核心程式碼提取並拆解:
int x = comb & -comb; //步驟(2)
int y = comb + x; //步驟(3)
int z = comb & ~y; //步驟(1)
int b = (z / x) >> 1; //步驟(4),'z/x'相當於去掉右側多餘的0,'>>1'則使剩下的1的個數減少一個
comb = b | y; //步驟(5)
(1)取出字典序最小的1的連續區間,1100 1100 → 0000 1100
(2)找到字典序最小的1的位置,1100 1100 → 0000 0100
(3)將字典序最小的1的連續區間置為0,並將區間左側第一個0置為1,1100 1100 → 1101 0000
(4)將 (1) 取出的區間右移,直至區間中1的個數減少一個,0000 1100 → 0000 0001
(5)將 (4) 的結果與 (3) 的結果取並集,0000 0001 | 1101 0000 → 1101 0001
按照這種方法,我們不難找出後續的子集:
1101 0010
、1101 0100
、1101 1000
、1110 0001
、1110 0010
、1110 0100
、1110 1000
、1111 0000
...
(正文完)
後記
發現這個演算法人也太優秀了吧!!太巧妙了!
(小白書:挑戰程式設計競賽)