一、基本概念
当集合元素个数较少时,可以用二进制来表示。集合{0, 1, ..., n - 1}
的子集S
可以用如下方法编码成整数:
f(S)=∑i∈S2i
本质就是用每一个二进制位来表示某个元素是否出现。
- 空集
ϕ
:
0
- 只含有第
i
个元素的集合{i}
:1 << i
- 含有全部
n
个元素的集合{0, 1, ..., n - 1}
:(1 << n) - 1
- 判断第
i
个元素是否属于集合S
:S >> i & 1
,orS & (1 << i)
- 向集合中加入第
i
个元素S U {i}
:S | 1 << i
- 向集合中去除第
i
个元素S \ {i}
:S & ~(1 << i)
- 集合
S
和T
的并集 S∪T :S | T
- 集合
S
和T
的交集 S∩T :S & T
- 枚举全集
S
的所有子集:for(int s = 0; s < (1 << n); s++) {/* 处理子集 */}
二、固定集合的子集遍历
问题:给定一个含有若干元素的集合s
(s是一个二进制数),枚举该集合的所有子集,含有n个元素的集合共有
2n
个子集。
分析:找到s
中1的个数和位置,然后通过删除若干个1来获得s
的所有子集。为了获得所有的子集可能需要对之前删掉的1进行二次访问,可以通过和原始集合s
进行与操作实现对之前删掉的1的二次访问。伪代码如下:
int subset = s;
do{
// 处理子集
subset = (subset - 1) & s;
}while(subset != s); // 当subset为-1的时候说明所有的子集已经遍历完
三、集合的K元素子集
问题:遍历一个集合的所有k
元素子集。例如,含有4个元素的所有子集为:00001111->00010111->00011011->...->11110000
分析:分四步解决:以00010111 --> 00011011
为例
1. 求出从最低位的1开始的连续的1的区间 (00010111 --> 00000111
)
2. 将此区间全部变为0,并将区间左侧的那个0变为1 (00010111 --> 00011000
)
3. 将第1步取出的区间右移,直到剩下的1的个数减少一个 (00000111 --> 00000011
)
4. 将第2步和第3步的结果异或 (00011000 | 00000011 = 00011011)
伪代码描述:
int comb = (1 << k) - 1; // 最小的 k 元素子集
while(comb < (1 << n)){
// 处理子集
/* 得到下一个子集 */
int x = comb & -comb; // x 表示最低位1的位置
int y = comb + x; // y 表示第2步操作的结果
comb = (((comb & ~y) / x) >> 1) | y;
}
python代码
#!/usr/bin/python
# -*- coding: utf-8 -*-
def subset(k, n):
comb = (1 << k) - 1
while comb < (1 << n):
print("next set: ", comb, bin(comb)[2:])
x = comb & -comb
y = comb + x
comb = ((int((comb & ~y) * 1.0 / x)) >> 1) | y
subset(4, 8)
四、子集生成
问题:给定一个集合,枚举所有可能的子集。
实现:
// 枚举子集
for(int s = 0; s < (1 << n); ++s){
// 输出子集S对应的各个元素
for(int i = 0; i < n; ++i){
if(s & (1 << i)) // or (s >> i) & 1
printf("%d ", i + 1);
}
printf("\n");
}