本文转自《挑战程序设计竞赛》 - p156 - 专栏: 集合的整数表示
集合的整数表示
在做一些算法题的时候经常需要表示一些集合的状态,如状压dp。
在程序中表示集合的方法有很多种,很直观的可以用数组直接模拟。
当元素个数较小时,可以考虑使用二进制码来表示(不超过32位时可以用int,不超过64时可以用long long)。
集合{0,1, … ,n-1}的子集S可以用如下方式编码成整数:
f(S) = Σ(2^i) (i∈S)
像这样表示后,一些集合运算可以对应写成如下形式:
基本操作
- 空集:
0
- 只含有第i个元素的集合{i}:
1<<i
; - 含有全部n个元素的集合{0,1,…,n-1}:
(1<<n)-1
- 判断第i个元素是否属于集合S:
if (S>>i & 1)
- 向集合中加入第i个元素S∪{i}:
S | (1<<i)
- 从集合中去除第i个元素S\{i}:
S & ~(1<<i)
- 集合S和T的并集S∪T:
S|T
- 集合S和T的交集S∩T:
S&T
枚举子集
- 枚举{0, 1, …, n-1}的全部子集
for (int S = 0 ; S < 1 << n; S++ ) {
// 对子集的处理
}
按照这个顺序进行循环的话,S便会从空集开始循环,然后按照{0}、{1}、{0,1}、…、{0,1,…,n-1}的升序枚举出来。
- 枚举某个指定集合sup的子集
int sub = sup;
do {
// 对子集处理
sub = (sub-1) & sup;
} while(sub != sup); // 处理完之后 会有 -1&sup = sup
- 枚举{0,1,…,n-1}所有大小为k的子集(有k个元素)
int comb = (1 << k) -1;
while ( comb < 1 << n ) {
// 对大小为k的集合的处理
int x = comb & -comb;
int y = comb + x;
comb = ((comb & ~y) / x >> 1) | y;
}
按照字典序,最小的大小为k的子集是(1<<k)-1
,所以用它做为初始集合。
现在求出comb后的下一个大小为k的子集的二进制码。
例如:
0010 1110
之后的是 0011 0011
, 0011 1110
之后的是0100 1111
。
下面是求出comb下一个二进制码的方法:
(1). 求出最低位的1开始的连续的1的区间(0010 1110
-> 0000 1110
)
(2). 将这一区间全部变为0,并将区间左侧的那个0变为1(0010 1110
-> 0011 0000
)
(3). 将第(1)步里取出的区间(连续1的那个)右移,直到剩下的1的个数减少了1个(0000 1110
-> 0000 0011
),也就是遇到第一个1移走就可以结束了。
(4). 将第(2)步和第(3)步的结果按位取或(0011 0000
| 0000 0011
-> 0011 0011
)
这里有个技巧,对于非零的整数,x & (-x)的值就是将其最低位的1独立出来之后的值。
将最低位的1取出后,设它为x。那么通过计算y=comb+x
,就将comb从最低位为1开始的连续的1都置0了。我们来比较一下~y和comb。在comb中加上x后没有变化的位,在~y中全部取相反的值。而最低位1开始的连续区间在~y中依然是1,区间左侧的那个0在~y中也依然是0。于是通过计算z=comb&~y
就得到了最低位1开始的连续区间。比如,如果comb = 0010 1110
,则x = 0000 0010
, y = 0011 0000
, z = 0000 1110
。
同时,y也恰好是第(2)步要求的值。那么首先将z不断右移,直到最低位为1,这通过计算z/x
即可完成。这样再将z/x
右移1位就得到了第(3)步要求的值。这样我们就求得了comb之后的下一个二进制列。因为是从n个元素的集合中进行选择,所以comb的值不能大于等于1<<n
。如此一来,就完成了大小为k的所有子集的枚举。