首页 > 其他 > 详细

一些子集生成的方法

时间:2020-12-13 15:05:10      阅读:22      评论:0      收藏:0      [点我收藏+]

增量构造法

增量构造法的基本思路是一次选择一个元素存放到集合中,之后递归输出集合。在使用增量构造法之前我们需要对集合元素的编号进行排列,这样的话就会避免相同的集合输出两次。这种方法称为定序。关于实现增量构造法的具体内容都在注释当中,这里就不再进行详解。


代码如下:

#include <iostream>
#include <cstdio>
using namespace std;
#define N 10                                //代表生成0-9的子集
void print_subset(int n, int* A, int cur);

void print_subset(int n, int* A, int cur) { //代表增量构造法的函数,代表一次取出一个元素到集合中
	int i = 0;
	for (i = 0; i < cur; i++) {               //代表输出集合
		printf("%d ", A[i]);
	}
	printf("\n");                             //输出集合后,要输出一个回车进行分隔(因此,当cur等于0时这里输出的是空集)
	int s = cur ? A[cur - 1] + 1 : 0;         //s代表确定当前元素的最小可能值(例如A中已经有了一个元素0,那么下次递归时cur就是1,那么当前元素的最小可能值就是A[cur-1]+1(因为A之中的元素编号都是从小到大排列的,这是定序的技巧(避免同一个集合枚举两次,例如:集合{1,2}输出时只能为{1,2}而不能为{2,1})
	for (i = s; i < n; i++) {                 //i代表从最小可能值开始一直遍历到n-1,作为取出的集合元素
		A[cur] = i;                           //将元素添加(取出)到集合A中(cur代表插入集合的位置)
		print_subset(n, A, cur + 1);          //进行递归(并将插入集合的位置+1)
	}
}
int main() {
	int A[10];                                //代表进行生成的集合A
	print_subset(N, A, 0);
}

位向量法

在这种方法中我们可以选择自己去构造一个位向量B[i],当B[i] = 1 时,代表元素i在该集合中,当B[i] = 0 时,代表元素i不在该集合中。类似于上述方式,我们需要对集合中的所有元素进行标记,才能确定一个子集。(例如:空集情况就是B[0-(n-1)]都为0)


代码如下:

#include <iostream>
#include <cstdio>
using namespace std;
#define N 10
void print_subset(int n, int* B, int cur);           //代表构造位向量法的函数

void print_subset(int n, int* B, int cur) {
	int i;
	if (cur == n) {                                  //当cur等于n时,代表一个集合已经构造完成
		for (i = 0; i < cur; i++) {                  //代表输出集合         
			if (B[i]) {                              //如果B[i]为1代表在集合中存在元素i,否则就不存在元素i
				printf("%d ", i);                    //输出元素i
			}
		}
		printf("\n");                                //输出回车以分隔集合
		return;                                      //输出完集合后,我们要进行返回(回溯)以便于输出下一个集合
	}
	B[cur] = 1;                                      //代表选择当前元素(cur)添加到集合中
	print_subset(n, B, cur + 1);                     //进行递归以便于选择下一个元素
	B[cur] = 0;                                      //代表不选择当前元素(cur)添加到集合中
	print_subset(n, B, cur + 1);                     //进行递归以便于选择下一个元素
}

int main() {
	int B[10];                                       //代表标记10个元素是否在集合当中的数组
	print_subset(N, B, 0);
}


二进制法(重点)

其实,我们可以利用二进制来表示{0,1,2 ... n-1}的集合S,具体方法是将二进制数从右往左第i位(各位从0开始编号)表示元素i是否在集合S中。示意图如下:

技术分享图片

上面的示例图将集合S用 0 1 0 0 0 1 1 0 0 0 1 1 0 1 1 1 这个二进制数来表示。从右往左开始,第一个1代表0在集合S中,第二个1代表1在集合S中,第三个1代表2在集合S中,第四个0代表3不再集合当中以此类推。其中0 1 2 为集合的元素也为存储二进制数的数组下标(只不过从右往左开始)


既然都可以用二进制来表示集合,那么我们同样可以使用二进制的运算符来对集合进行运算。常见的二进制运算符如下:


  • & 代表按位与运算,如果两个二进制数进行按位与运算时,只有对应位上都是1,结果才是1,否则为0。(对应着集合的交集)
  • | 代表按位或运算,如果两个二进制数进行按位或运算时,只有对应位上都是0,结果才是0,否则为1。(对应着集合的并集)
  • ! 代表按位取反运算,也称为非运算。这个运算符只能对一个二进制数进行运算,它的作用是将1变成0,将0变成1
  • << 代表左移位运算,左移位运算相当于乘以2的n次幂。形式为:乘数 << n次幂 例如:1<< 2 相当于1乘4(2的2次幂) 在二进制的运算中,左移位代表将一个二进制数从左去除2位,之后在二进制数的最右侧用0补充。例如:0 0 0 0 0 0 1 0 << 2 就为 0 0 0 0 1 0 0 0 其中 0 0 0 0 0 0 1 0 转换成10进制为2。 0 0 0 0 1 0 0 0 转换成10进制为8。
  • >> 代表右移位运算,右移位运算相当于除以2的n次幂。形式为:乘数 >> n次幂 在二进制的运算中,右移位代表将一个二进制数从右去除2位,之后再二进制数的最左侧,如果当前二进制数的最高位(去位数之前)是1,则最左侧补1,如果当前二进制数的最高位是0,则最左侧补0。
  • ^ 代表按位异或运算,如果两个二进制数进行按位异或运算时,当对应位上的数相同时,结果为0。当对应位上的数不同时,结果为1(对应着集合的对称差集)

在这里科普一下对称差集:对称差集:集合A与集合B的对称差集定义为集合A与集合B中所有不属于A∩B的元素的集合,记为A△B。


因此,我们可以根据上面的二进制运算符来对集合进行操作。列表如下:

技术分享图片

另外,我们还可以把全集定义为:ALL_BITS = (1<<n)-1 ,则A的补集就是ALL_BITS^A。我们也可以用整数减法ALL_BITS -A 也可以,但速度比异或要慢。


因此,熟悉了上述的内容后,使用二进制法生成集合的代码如下:

#include <iostream>
#include <cstdio>
using namespace std;
#define N 10
void print_subset(int n, int s);              //代表构造二进制法的函数
void print_subset(int n, int s) {             //代表构造二进制法并输出集合的函数
	int i;
	for (i = 0; i < n; i++) {                 
		if (s & (1 << i)) {
			printf("%d ", i);
		}
	}
	printf("\n");
}
int main() {
	int i;
	for (i = 0; i < (1 << N); i++) {        //代表遍历子集,其中(1<<N的意思为:对于有n个元素的集合,它的子集为2的n次方个)。那么,i就代表遍历的子集。
		print_subset(N, i);
	}
}

for (i = 0; i < n; i++) {                 
		if (s & (1 << i)) {
			printf("%d ", i);
		}
	}

s & ( 1<< i ) 这条代码的意思为:由于1 << i 可以生成 0000000001、0000000010、0000000100、0000001000这样的二进制数(因为i从0遍历到n-1且使用了移位运算)其实以上生成的二进制数的作用是对输入集合s的每一位进行检验,通过对遍历的集合s进行按位与运算后,就可以生成集合。这样说你可能听不明白,再此我们举个例子:


假设s为1,转换成2进制数为0000000001,由于i从0遍历到n-1,因此1 << i 会生成以下二进制数:

  • 0 0 0 0 0 0 0 0 0 1
  • 0 0 0 0 0 0 0 0 1 0
  • 0 0 0 0 0 0 0 1 0 0
  • 0 0 0 0 0 0 1 0 0 0
  • 0 0 0 0 0 1 0 0 0 0
  • 0 0 0 0 1 0 0 0 0 0
  • 0 0 0 1 0 0 0 0 0 0
  • 0 0 1 0 0 0 0 0 0 0
  • 0 1 0 0 0 0 0 0 0 0
  • 1 0 0 0 0 0 0 0 0 0

那么我们就可以将s与这些二进制数进行按位与运算,最终得出生成的集合。那就为:

  • 0 0 0 0 0 0 0 0 0 1 & 0 0 0 0 0 0 0 0 0 1 = 0 0 0 0 0 0 0 0 0 1 转换成10进制为1(代表元素0在集合中,那么输出的集合元素为0 ,此刻i为0)
  • 0 0 0 0 0 0 0 0 0 1 & 0 0 0 0 0 0 0 0 1 0 = 0 0 0 0 0 0 0 0 0 0 转换成10进制为0 (代表元素1没在集合中,那么就不输出集合元素1 此刻i为1)
  • 0 0 0 0 0 0 0 0 0 1 & 0 0 0 0 0 0 0 1 0 0 = 0 0 0 0 0 0 0 0 0 0 转换成10进制为0 (代表元素2没在集合中,那么就不输出集合元素2,刺客i为2)
  • 依此类推,这里就不再讲解

等到i遍历到n时,退出循环,此刻屏幕上输出的只有0元素,那么生成的集合为{0},其他数也是一样的生成方法,这里就不再过多讲解。


接下来的操作依次类推,随着s的值不同,输出的集合也互不相同,那也就是说当s与(1<<i)进行按位与运算后转换成10进制数,如果为0,就不进行输出。如果为非0,则执行后面的输出语句,来输出集合。


因此,对比以上的子集生成方法,从代码量上看最简单的就是二进制法!

一些子集生成的方法

原文:https://www.cnblogs.com/gao79135/p/14128453.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!