以下所有操作均可以通过递归DFS每一位并作相应判断实现。但以下写法实现简洁、常数较小,不失为一种优美的技巧。
下文采用Si表示第i个集合,msk表示其二进制表示。
简单集合运算
- 并:S1∪S2←msk1∣msk2
- 交:S1∩S2←msk1 & msk2
- 差:S1∖S2←msk1 & (∼msk2)
- (相对于该二进制数类型最大值的)补集:∼msk
- 对称差集:S1ΔS2←msk1 ∧ msk2
子集与超集
-
枚举集合S的所有子集。
考虑二进制数msk0=S,依次枚举字典序更小的二进制表示。有msk’←(msk−1) & msk0,即取枚举到的集合与S的交,直到msk=0(空集)。显然可以不重不漏地枚举完成所有子集。
当然,也可以通过lowbit(msk)
运算取出集合的所有元素,然后建立[0,2∣S∣)和子集的映射,枚举连续的二进制数即可。
-
枚举集合S的所有超集。
同理,依次枚举字典序更大的集合。有msk’←(msk+1)∣msk0,即取枚举到的集合与S的并集。直到超过给定的某个限定值。
-
枚举集合S的所有大小为k的子集。
首先通过lowbit(msk)
取出S中所有元素,建立[0,2∣S∣)对所有子集的映射。
按照字典序从小到大枚举。具体地,我们以msk=2k−1作为第一个枚举的集合,然后依次进行以下操作:
- 记l=lowbit(msk),p=msk+l。
- 现在我们得到集合p,由于l导致了至少一次进位,故popcount(p)≤k。
- 考虑找出在msk中,紧挨l的所有更高位的二进制位1。它们都因进位而变成0。因此,有q=msk & ∼p表示上述0→1的位。
- 现在我们将这缺失的若干位补足到p的后面,即msk’=p+2lq,也即将这连续的若干位向右移动到最低位,去除最高位1,并取它们和p的并。
本质上,如果l的二进制表示左侧有c>0个连续的1,则在字典序表示中,在这c位1的左侧状态不变的情况下,msk所表示的已经是能够达到的字典序最大的集合。因此,我们被迫移动(进位)第c位1,在新的状态下,msk’最低位的c个1就是最小字典序的子集。例如0b101011100→0b101100000→0b101100011。
- 1
- 2
- 3
- 4
- 5
- 6
for (int msk = (1<<k)-1; msk < (1<<n);) {
int l = msk & -msk, p = msk + l;
msk = ((msk & ~p) / l >> 1) | p;
}