集合与二进制的联系
集合可以用二进制表示,二进制从低到高第 i位
i 位为 1,表示 i 在集合中,
i 位为 0,表示 i 不在集合中。
例如集合 {0,2,3}
{0,2,3} 可以用二进制数 1101表示;反过来,二进制数 1101;1101 就对应着集合 {0,2,3}。
对于某个非负整数的集合中,可以用数字表示:
集合与集合
其中 && 表示按位与,∣∣ 表示按位或,⊕⊕ 表示按位异或,∼∼ 表示按位取反。
其中「对称差」指仅在其中一个集合的元素。
子集的判断
这时如果我们添加一个元素,并且这个元素包含了 0 , 2 , 3,如29,用二进制表示:1 1101,29包含了集合中的所有元素(包含了元素的所有的1),意味着29是最大的集合
由此我们引出了集合a是集合b的子集用二进制的表示方法:
a&b = a,表示a是b的子集,反之则不是
a | b = b,等价于上
集合与元素
S & (S-1)
如何快速找到下一个子集呢?以 101100→101001 为例说明,
普通的二进制减法会把最低位的 1 变成 0,同时 1 右边的 0 变成 1,即 101100→101011。
「压缩版」的二进制减法也是类似的,把最低位的 1 变成 0,同时对于 1右边的 0 变成 1,只保留在 s=101101 中的 1所以是 101000→10001。怎么保留?& 10101 就行。
s = 101100
s-1 = 101011 // 最低位的 1 变成 0,同时 1 右边的 0 都取反,变成 1
s & (s-1) = 101000这里要注意:
1.第三步计算后的最小元素作为新的s是从右到左找第i位为1的元素
如此原本为{2,3,5}的集合删除了最小的元素后变成了{3,5} 。这意味着如果s的二进制位上只有一个1或者都是0时(2的幂),s&(s-1)后的结果会删除这个1,等于0
2.如果初始S维护起来,计算后的最小元素用变量来接收,用这个每一次枚举都要更新变量的值&S,则是找出全部的非空子集。
//设集合为 s,从大到小枚举 s 的所有非空子集 sub for (int sub = s; sub; sub = (sub - 1) & s) { // 处理 sub 的逻辑 }
---------------------------------------------------------------------------------------------------------------------------------
介绍完s & (s-1)(集合的缩小),下面就要介绍怎么得到最小的集合,也就是怎么得到以最低位的1为最高位的二进制数,如100 1100中的 100
S & (-S)
这里仍用集合{2,3,5}演示过程
s = 101100 ~s = 010011
-s = 010100 //相反数等于补码取反+1
s & (-s) = 000100
如此我们取到了第一个最小的元素 2 ,接下来要取到第二小的元素呢?
不难猜出->我们要在此之前删除这个用过的最低为的1,那么就要用到异或操作(也可以是减法)
我们用lowbit来记录每一次计算出来的最小元素
lowbit = s & (-s) = 000100
S = S ^ lowbit = 101100
^ 000100
S = 101000
如此反复就能完成每一次的最小元素的取出了。
代码实现:
下面是对于以上的代码实现:
为了方便展示和观察,示例用的是 21
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <intrin.h>
using namespace std;
class Solution
{
public:
void NumbersInSet(int num)/*统计集合中的每个元素(从大到小枚举集合的所有非空子集)
10101 -> 10100 -> 10001 -> 10000 -> 00101 -> 00100 -> 00001
相当于缩减版的二进制减法 1 1 1 1 1 0 1 0 1 1 0 0 0 1 1 0 1 0 0 0 1
*/
{
int lowbit = num & (num - 1);
while (lowbit)
{
cout << lowbit << " ";
lowbit = num & (lowbit - 1);
}
}
void NumbersInSetExtension(int num)/*扩展最大元素,包含着此时的子集的更大集合
10101 -> 10111 -> 11101 -> 11111 ->110101 -> 110111 -> 111101 -> 111111
( 21 ) ( 23 ) ( 29 ) ( 31 ) ( 53 ) ( 55 ) ( 61 ) ( 63 )
_0_0_ _0_1_ _1_0_ _1_1_ 1_0_0_ 1_0_1_ 1_1_0_ 1_1_1_
相当于缩减版的二进制加法
*/
{
int n = 6;
int j = 0;
int lowbit;
long long ans = num;
int x = num;
x = ~x;//此时的二进制的1就是之前的0,可以填入的位置
int count = 0;
while (++count <= n)//从 0 1 开始填入 直到为n
{
while (count >> j)
{
lowbit = x & (-x);//num集合中最小元素
ans |= (long long)lowbit * ((count >> j) & 1);
++j;
x ^= lowbit;//差
}
cout << ans << " ";
j = 0;
x = ~num;
ans = num;//重新选择新的num填入count的二进制位
}
}
};
int main()
{
int n;
cin >> n;
Solution test;
test.NumbersInSet(n);
cout << endl;
test.NumbersInSetExtension(n);
return 0;
}
文中的~x是为了方便快速找到可以填入的空位(空位取反后是1)
以上便是对集合的内的元素/非空子集遍历和元素的扩展遍历