使用位运算得到所有子集
在紫书上看到的,挺有意思。
一看到位运算我就会躲,因为我整不明白。
代码
#include "iostream"
#include "cstdio"
using namespace std;
void subset(int n, int s) {
printf("{");
for (int i = 0; i < n; i++)
if (s & (1 << i)) printf("%d ", i+1);
printf("}\n");
}
int main() {
int n;
while (scanf("%d", &n) != EOF)
for (int i = 0; i < (1 << n); i++)
subset(n, i);
return 0;
}
这段代码就是输入n,输出1~n的所有子集。
位运算是玄学,但是时空效率和代码长度都比之前使用其他的递归方法来计算好一些。当然当n过大还是会不行。
思路
可以使用一串二进制数来表示一个1~n的子集,从右往左分别是\(2^0,2^1,2^2,...,2^n-1\)。
下图代表全集\(Q=\{x|1\le x\le 16\}\)中的一个子集,书上的最右侧是0,但我们选择从1开始。
当第i位为0时,我们就认为子集中不包含这个元素,当第i位为1时,认为包含这个元素,所以上图是代表集合\(S=\{1,2,3,5,6,10,11,15\}\)。这里注意因为我们的最右侧是从1开始的,而上图是从0开始的。
那么这个n位的二进制数足以通过其中1/0的状态不同来表示1~n的所有子集。这个n位的二进制数的十进制范围就是\(0-2^{n-1}\)。
下面我们来介绍两个基本操作,与和左移位。会的直接跳过。
位与操作自然不用我说,对应位做与运算,与操作对应的集合操作是求交集,如{1,0,1} & {0,1,1} = {0,0,1}
。转换成上面的集合表示就是{3,1} & {2,1} = {1}
。
左移一位相当于乘2操作,\(0001 << 1 = 0010\),\(0010 << 1 = 0100\)。转换成十进制就是\(1*2=2和2*2=4\)。
下面我们来看代码。
首先是主函数中的这一段:
for (int i = 0; i < (1 << n); i++)
subset(n, i);
循环遍历\(0到2^{n-1}\)中的每一个数,这些数就构成了\(1到n\)的所有子集。相当于对每一个子集调用subset来输出。而且i单调递增,这也能保证输出的东西是字典序的。
然后是subset
for (int i = 0; i < n; i++)
if (s & (1 << i)) printf("%d ", i+1);
这个循环相当于打印出第i位的值为1的数。(1<<i)
会产生诸如00001,00010,00100,01000,10000
这样的数去检查s
的每一位,然后和s做交集,这样产生的结果就是当第i位为1时条件就会为真,当第i位为0时(也就是不在子集中)条件就会为0(因为&操作得到了一个空集)。