问题描述:
给定一个集合,比如{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;
}