C++算法初级3——子集枚举

子集枚举

1. 引入

子集枚举,顾名思义就是在枚举所有子集。为了解释这句话,我们需要先明确几个概念:

集合: 集合就是包含一些对象的整体。
比如一个学校里,“全体同学”就是包含所有同学的集合;“全体男生”就是包含所有男同学的集合,“全体女生”就是包含所有女同学的集合。

子集: 子集就是只包含某集合中一部分对象的集合。
比如在学校中的“全体同学”里,“经管系的同学”就是“全体同学”的一个子集。另外,每个非空集合都有两个最特殊的子集。一个是空集,不包含该集合中的任何元素。另一个是该集合本身,包含该集合中的所有元素。

了解了集合和子集的概念以后,我们来看一个例题:

【举例】

假设我们有个集合{1,2,3,…,n},输出所有满足集合中所有数求和是3的倍数的子集的个数。

【思路】

回顾一下枚举的基本思想:

确定枚举对象、枚举范围和判定条件;
枚举可能的解,验证是否是问题的解。

我们也可以用这个思路解决该问题,那么该题的算法就是:

步骤1:枚举所有子集S。
步骤2:如果枚举到的子集S满足条件,则将其计入答案。
对于步骤2,算法设计是比较直接的。给定集合S,要想检查它是否满足“所有数求和是3的倍数”这个条件,只需要将所有的数加起来,模3,检查结果是否为0即可。

但是对于步骤1,我们如何枚举一个集合的所有子集呢?这就要涉及到我们的子集枚举算法。

2. 子集的表示方式

a. 数组表示法

首先,我们需要明确子集在计算机中的表示方式。并且,如果想让计算机高效地枚举一个对象,我们需要一个能利于计算机这样做的表示方式。数组表示方法是最直接的表示方法

b. 子集的表示方式 – 01比特串法

我们可以类比“字符串”,而将“比特串”类比为一串01数字。我们通过如下方式用长度为n的01比特串表示一个大小为n的集合 { a 1 , … , a n } \{a_1, \ldots, a_n\} {a1,,an}的子集:

如果01比特串的第i个元素是0,表示在该子集中没有包含第i个元素,相反如果第i个元素是1,则表示该子集中包含了第i个元素。

在这里插入图片描述
对于一个大小为n的集合,它的所有子集对应着所有00…0到11…1的长度为n的比特串。

而长度为n的比特串又对应着范围从0到 2 n − 1 2^n-1 2n1的整数。

所以,如果我们想枚举所有子集的话,就是在枚举该子集的比特串表示,就是在枚举该比特串的整数。
下面,我们先解决两个比较关键的问题,然后再给出完整代码:

  • 枚举整数的范围:从0(空集)枚举到 2 n − 1 2^n - 1 2n1(也就是原集合本身)
    因为对我们有用的只有长度为n的01比特串,超过n以后就没有意义了。所以如果转化成整数的话,最大的n位二进制整数是 2 n 2^n 2n 。所以枚举整数的时候,应该从0(空集)枚举到 2 n − 1 2^n - 1 2n1(也就是原集合本身)

  • 如何知道第i个元素是否在集合里。
    因为我们在枚举的时候枚举的是整数而不是直接枚举01比特串。所以怎样知道一个整数num在二进制表示下第i位是0还是1呢?(这里第i位为从最低位开始数的第i位)

    这里我们用一些位运算的技巧。

    首先考虑一个比较简单的问题:怎样知道最低位是0还是1呢?我们只需要计算num & 1即可(&为与操作),根据一个例子感受一下这么做的原因:在这里插入图片描述
    另外,num & 1也可用于检查数字num的奇偶性(这是因为当数字num是奇数时,二进制表示下的最低位就是1,而如果它是偶数,那么二进制表示下最低位就是0。所以检查奇偶性和检查二进制位最低位是等价的。)

    那么如果想检查第2位怎么办呢?

    我们可以想办法把检查第2位转换成检查第1位这个问题。具体转换方法,就是将整个二进制数字,向右平移一位。
    对于向右平移一位,位运算中有对应的右移运算符>>;右移运算符在计算结果上,相当于“除以2下取整”; 在视觉效果上,就相当于将二进制表示下的数字丢掉最低位,并向右移一位;

    这里,我们可以拿十进制做类比:在十进制下,“除以10下取整“就相当于在十进制下去掉最低位,向右平移1位,例如:
    在这里插入图片描述

c. 递归枚举子集

之前我们给出了用数组表示集合的方法。那么有没有一种枚举方式可以基于这种表示方法来枚举集合的所有子集呢?

答案是有的,在以后学习递归的过程中,我们可以写一个递归函数,生成表示每个子集的对应数组。

这里我们简单提一下,其本质思想就是分情况讨论:

在这里插入图片描述
在这个结构中,第i层的结点表示的是基于上面的结点对应的子集再加入一个元素的可能情况。

所以,我们就按照这个树形结构枚举了所有的子集。

以在实现上,我们需要用一种叫做“递归”的方式将这个树形结构在代码中生成出来。

使用这种基于数组表示法和递归方法枚举子集的好处,是因为它的复杂度是 O ( 2 n ) O(2^n) O(2n),要好于01比特串的枚举方法。

3. 代码实现

#include <bits/stdc++.h>
using namespace std;
int n;
int main() {
    scanf("%d", &n);    // 集合大小,也就是01比特串的长度
    int tot = 1 << n;   // 枚举数字代替01比特串,范围为0到2^n - 1
    int ans = -1;       // 消除空集
    for (int num = 0; num < tot; ++num) {  // 枚举每个代表01比特串的数字
        long long sum = 0;
        for (int i = 0; i < n; ++i)        // 枚举01比特串的每一位
            if ((num >> i) & 1) {          // 检查第j位是否为1,注意这里是从0开始枚举
                sum += (i + 1);            // 如果该位是1,就把对应的数字加到求和的变量里
            }
        if (sum % 3 == 0) ++ans;           // 如果满足题目要求(3的倍数),计入答案
    }
    printf("%d\n", ans);
}

01比特串复杂度分析

用01比特串进行子集枚举,根据上面的代码我们可以给出该算法的复杂度分析:

第一层for循环,一共循环 2 n 2^n 2n

for循环里,枚举n个二进制位的每一位,复杂度为O(n)

所以最终复杂度为 O ( 2 n ⋅ n ) O(2^n\cdot n) O(2nn)

  • 11
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值