子集生成的三种算法《算法竞赛入门经典》(紫书)

问题描述:

给定一个集合,比如{1,2,3},要生成所有的子集(不包括空子集,也就是2n-1个集合)。

方法1:增量构造法

(这个名字看起来好像不好理解,其实往下看你会觉得挺好理解的。)
核心思路在已有子集的基础上不断增加新元素一直到无法继续增加为止,对于每一个元素都判断一下看要不要增加新的元素。
对于集合s = 1,2,3
比如已经确定了第一个元素1,然后在这个基础之上,确定要不要增加2or3,增加2,就变成了1,2,增加3,就变成了1,3
此时1,2也要判断看是不是要增加3,如果增加就变成了1,2,3
但是1,3就不能判断还要不要增加元素了,因为5的后面已经没有元素了,
同样当第一个元素为2的时候,也是一样的运算过程。

设置一个辅助数组b,使用cur一步步的指定遍历到的位置,然后每次递归进函数都输出一次。
传入的集合a中元素的数量n,集合a,辅助数组b ,当前指向的位置cur。

代码1:

void print_subset_1(int n,int * a,int * b,int cur)
{
    for(int i = 0; i < cur; i++)
    {
        cout << a[b[i]] << " ";
    }
    cout << endl;

    int s = cur ? b[cur - 1] + 1 : 0;
    for(int i = s; i < n; i++)
    {
        b[cur] = i;
        print_subset_1(n,a,b,cur+1);
    }
}
方法2:位向量法

很容易想到的一个方法就是:每个元素都对应一个标记位,标记位为true表示存在,标记位为false表示不存在,所以就引出了这种方法。
通过递归调用函数就会生成一颗子集树,树的叶子节点表示了一个子集的情况(个数为n的01序列),
被选了的,也就是在标记数组中为true的就输出,没有被选的,也就是为false的就不输出
只有到最后一层(因为每次都要给每个元素都设定一个标记位,表示选or不选)的叶子节点才会输出,也就是cur == n。
位向量就是一个标志数组,来记录每个元素的状态
注:
1、生成子集的顺序与增量构造法不一样
2、位向量法会比增量构造法的效率要低(在数据规模小的时候差别不明显),
因为这个算法要求让所有的元素都遍历完成时才能输出(这也就是直到cur == n才输出的原因)
3. 代码的传参为:集合a元素的数量n,要求子集的数组a,位向量数组b,当前指向的位置cur

代码:

void print_subset_2(int n,int * a,bool * b,int cur)
{
    if(cur == n)
    {
        for(int i = 0; i < n; i++)
        {
            if(b[i] == true)
            {
                //cout << i + 1 << " ";
                cout << a[i] << " ";
            }
        }
        cout << endl;
        return;
    }
    b[cur] = false;
    print_subset_2(n,a,b,cur+1);
    b[cur] = true;
    print_subset_2(n,a,b,cur+1);
}
方法3:二进制法

二进制法和位向量法有相似的地方,就是用0和1来表示元素是不是在集合中
此时用二进制数能够很好的解决问题,主要是空间复杂度得到了优化,时间效率也高

补:位运算
1、& 按位与: 1&1 = 1 \ 1&0 = 0 \ 0&1 = 0 \ 0&0 = 0(只要有0,就得0)
2、| 按位或: 1|1 = 1 \ 1|0 = 1 \ 0|1 = 1 \ 0|0 = 0(只要有1,就得1)
3、^ 按位异或: 参加运算的两个二进制数对应的位数相同 => 0 \ 不相同 => 1
4. A▲B -> A对B取对称差
A▲B = (A-B)∪(B-A)
= (A∪B)-(A∩B)
===> A的原本有的 + B中有A中没有的 - AB都有的
特点:
1. 1 ^ 任何数 = 任何数取反
2. 0 ^ 任何数 = 任何数
3. 任何数 ^ 任何数 = 0
常见用途:
1. 使某些特点的位翻转
例如:使10100001的第2位和第3位翻转(是从右往左开始数)
00000110 ^ 10100001 = 10100111
2. 实现不用中间变量的值的交换
例如:要交换a = 111(7) \ b = 100(4)
a = a^b;//a = 011 = 111 ^ 100
b = b^a;//b = 111 = 100 ^ 011
a = a^b;//a = 100 = 111 ^ 011
交换成功 => a = 100(4) \ b = 111(7)
4、~ 按位取反: 将一个二进制数的每一位都取反 0 => 1 \ 1 => 0
5、<< 左移运算符: 将一个数的各二进制位全部左移n为,右补0
例如:
int a = 15;(00001111)
a << 2 ===> 00111100(60)
二进制数左移一位相当于十进制数乘2
6、>> 右移运算符: 将一个数的各二进制位全部右移n为,左补0
例如:
int b = 60;(00111100)
a >> 2 ===> 00001111(15)
二进制数右移一位相当于十进制数除2

但是这个方法,我也不是很懂,直接看代码吧:
代码:

void print_subset_3(int n,int * a)
{
    for(int i = 0; i < (1<<n); i++)
    {
        //cout << "i = " << i << endl;
        for(int j = 0; j < n; j++)
        {
            //cout << "j = " << j << endl;
            //cout << "1<<j = " << (1<<j) << endl;//(1<<j)要加上括号,不然当作两个打印了
            if(i & (1<<j))
            {
                //cout << "满足条件: ";
                cout << a[j] << " ";
            }
        }
        cout << endl;
    }
}

最后给出一个我用于测试这三种算法的代码

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <algorithm>
using namespace std;

const int MAX = 10;

/* 方法一:增量构造法 */
void print_subset_1(int n,int * a,int * b,int cur)
{
    for(int i = 0; i < cur; i++)
    {
        cout << a[b[i]] << " ";
    }
    cout << endl;

    int s = cur ? b[cur - 1] + 1 : 0;
    for(int i = s; i < n; i++)
    {
        b[cur] = i;
        print_subset_1(n,a,b,cur+1);
    }
}

/* 方法二:位向量法 */
void print_subset_2(int n,int * a,bool * b,int cur)
{
    if(cur == n)
    {
        for(int i = 0; i < n; i++)
        {
            if(b[i] == true)
            {
                //cout << i + 1 << " ";
                cout << a[i] << " ";
            }
        }
        cout << endl;
        return;
    }
    b[cur] = false;
    print_subset_2(n,a,b,cur+1);
    b[cur] = true;
    print_subset_2(n,a,b,cur+1);
}

/* 方法三:二进制法 */
void print_subset_3(int n,int * a)
{
    for(int i = 0; i < (1<<n); i++)
    {
        //cout << "i = " << i << endl;
        for(int j = 0; j < n; j++)
        {
            //cout << "j = " << j << endl;
            //cout << "1<<j = " << (1<<j) << endl;//(1<<j)要加上括号,不然当作两个打印了
            if(i & (1<<j))
            {
                //cout << "满足条件: ";
                cout << a[j] << " ";
            }
        }
        cout << endl;
    }
}

int main()
{
    srand(time(NULL));
    int n;
    while(cin >> n)
    {
        /**
            输入一个n,表示集合中元素的个数,元素的排列就是1-n
            利用三种不同的算法来输出集合的所有子集。
        */
        int a[MAX];
        for(int i = 0; i < n; i++)
        {
            //a[i] = i+100;
            a[i] = rand()%100+1;
            cout << a[i] << " ";
        }
        cout << endl;
        /** 方法一:增量构造法
        int b[MAX];
        memset(b,0,sizeof(b));
        print_subset_1(n,a,b,0);*/

        /** 方法二:位向量法
        bool b[MAX];
        memset(b,false,sizeof(b));
        print_subset_2(n,a,b,0);*/

        /** 方法三:二进制法
        print_subset_3(n,a);*/

    }
    return 0;
}

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值