常用技巧——集合的二进制整数表示(挑战程序设计竞赛)
在程序中表示集合的方法有很多种,当元素比较少时,像这样,用二进制码来表示比较方便。集合{0,1,2…n-1}的子集可以用以下方式编码成整数。
换而言之,一个二进制数的某一位上,如果这第i位是1,表示i属于这个集合,否则就不属于。这样表示之后,可进行如下操作:
空集.............................0
只含有第i个元素的集合{i}............1<<i
含有全部n个元素的集合{0,1...n-1}....(1<<n)-1//含有n个元素的全集
判断第i个元素是否属于集合S...........if(S>>i&1)
向集合中加入第i个元素S∪{i}..........s|1<<i
从集合中取出第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的子集。这里sup是一个二进制码,其本身也是某个集合的子集。例如给定了01101101这样的集合,要将01100000或者00101101等子集枚举出来。前面是从0开始不断加1来枚举出了全部的子集。此时,sub+1并不一定是sup的子集。而(sup+1)&sup虽然是sup的子集,可是很有可能依旧是sub,没有任何改变。
所以我们要反过来,从sup开始每次减1,直到0为止。由于sub-1并不一定是sup 的子集,所以我们把它与sup进行按位与&,这样的话就可以将sup所有的子集按照降序列举出来。(sub-1)&sup会忽略sup中的0而从sub中减去1。
int sub = sup; //子集
do{
sub = (sub - 1) & sup;
}while(sub != sup); //处理完0以后,会有-1&sup = sup;
最后我们介绍一下枚举{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;
}
按照字典序的话,最小的子集是(1<< k)-1,所以用它作为初始值。现在我们求出comb其后的二进制码。例如0101110之后的是0110011,0111110之后的是1001111。下面是求出comb下一个二进制码的方法。
(1)求出最低位的1开始的连续的1的区间。
(0101110 -> 0001110)
(2)将这一区间全部变为0,并将区间左侧的那个0变为1。
(0101110 -> 0110000)
(3)将第一步里取出的区间右移,直到剩下的1的个数减少了一个。
(0001110 -> 0000011)
(4)将第二步和第三步中的结果按位取或(0110000|0000011 = 0110011)
对于非零的整数, x&(-x)的值就是将其最低位的1独立出来后的值,这部部分内容在树状数组部分有详细讲解,这里就不赘述了。
将最低位的1取出后,设它为x。那么通过计算y = comb + x,就将comb从最低位的1开始的连续的1都置零了。我们来比较一下y和comb。在comb中加上x后没有变化的位,在y中全都取相反的值。而最低位1开始的连续区间在y中依然是1,区间左侧的那个0在y中依然也是0.于是通过计算z = comb & ~y 就得到了最低位1开始的连续区间。比如:如果comb = 0101110,则 x = 0000010,y = 0110000,z = 0001110.
同时,y也恰好是第二步要求的值。那么首先将z不断右移,直到最低位为1,这通过计算z/x即可完成。这样再将z/x右移1位,就得到了第三步要求的值。这样我们就求得了comb之后的下一个二进制列。因为是从n个元素的集合中进行选择,所以comb的值不能大于等于
1*2^n,如此一来,就完成了大小为k的所有子集的枚举。
除了上述例子之外,还可以利用位运算完成满足其他条件的集合的枚举,例如不包含相邻元素的集合等。
另附几个常用的二进制处理:
i&1:取i的最右位(个位)
i=i|1:将最右位(个位)清1
i=i&(~1):将最右位(个位)清0
i&-i:在树状数组中会用到,取从右往左的第一位1
补充:
集合运算中| 和 ^是有区别的(废话
|是随便有一个是1结果就是1 所以用来取∪
而^是不一样为1 一样为0 和1异或可以用来取反