【蓝桥杯】二进制枚举子集

二进制法

给定一个集合,枚举所有可能的子集。枚举子集的方法有很多,本节我们介绍一种代码编写非常方便的枚举子集方法——二进制法

我们可以用二进制的一位表示集合对应某一元素的选取状态, 1 1 表示选取, 0 0 表示未选取。

举个例子,我们拥有一个集合 { 0 , 1 , 2 , 3 , 4 , 5 , 6 {0,1,2,3,4,5,6} }。

那么二进制0101101就代表子集合 { 0 , 2 , 3 , 5 {0,2,3,5} },如下图所示。

2进制数位 6 5 4 3 2 1 0
二进制制数值 0 1 0 1 1 0 1
选取的元素 - 5 - 3 2 - 0

位运算

代码中对于二进制的处理可以用位运算来实现。位运算是对二进制的每一位进行计算,所以每一位只有 0 0 1 1 两种可能。先介绍三种常用的位运算符:与 &、或|、亦或^,运算规则如下表所示

A B A&B A|B A^B
0 0 0 0 0
0 1 0 1 1
1 0 0 1 1
1 1 1 1 0
  • 与运算:两者都为 1 1 时,结果为 1 1 ,否则为 0 0
  • 或运算:两者都为 0 0 时,结果为 0 0 ,否则为 1 1
  • 异或运算:两者同为 0 0 或者 1 1 时,结果为 1 1 ,否则为 0 0

两个十进制整数进行位运算结果是多少呢?举个例子 A = 25 与 B = 14 做位运算,A 转化为二进制是 11001,B 转化为二进制是 01110,那么结果如下图

A = 25 1 1 0 0 1
B = 14 0 1 1 1 0
A&B = 8 0 1 0 0 0
A|B = 31 1 1 1 1 1
A^B = 23 1 0 1 1 1

注意: 不要把 A^B 当成 A B A^B

位运算符

位运算符中有两种操作,左移<<和右移>>。右移具体还分为带符号右移与无符号右移,本节我们提到的右移指的是带符号右移,无符号右移使用较少,在这里不做解释。

对于A << B,表示把 A 转化为二进制后向左移动 B 位(在末尾添加 B 个 0 )。

对于A >> B,表示把 A 转化为二进制后向右移动 B 位(删除末尾的 B 位)。

比如2 << 2, 2 转化为二进制则是 10,那么就是 10 左移动 2 位,就变成了二进制1000,转化为十进制是 8,所以 2 << 2 = 8 。

举个例子

我们来看一个可以用二进制枚举的方法解决的题目。

话说大诗人李白,一生好饮。幸好他从不开车。

—天,他提着酒壶,从家里出来,酒壶中有酒两斗。他边走边唱:

  • 无事街上走,提壶去打酒。
  • 逢店加一倍,遇花喝一斗。

这一路上,他一共遇到店 5 次,遇到花 10 次,已知最后一次遇到的是花,他正好把酒喝光了。请你计算李白遇到店和花的次序,有多少种可能的方案。

解答

这个题目解法很多,二进制枚举是一种写起来非常简洁的解法。我们已知遇店 5 次,遇花 10 次,并且最后一次遇到花,正好把酒喝光。那么我们可以把店作为二进制中的 1,把花作为二进制中的 0,因为已经确定最后一次遇到的是花,所以我们需要判断枚举的结果是否刚好有 5 个 1 和 9 个 0。那么我们就枚举出 14 位二进制的所有可能并加以判断即可,判断思路为判断二进制是否有 9 个 0,5 个 1,并且最终酒刚好剩 1 斗。

根据这个思路,可以写出如下的代码:

int ans = 0;
for (int i = 0; i < (1<<14); ++i) {
    int tot_1 = 0;
    int tot_0 = 0;
    int num = 2;
    for (int j = 0; j < 14; ++j) {
        if (i&(1 << j)) { // 这里判断二进制 i 从右数第 j + 1 位是否为 1
            tot_1++;
            num = num * 2;
        } else {
            tot_0++;
            num = num - 1;
        }  
    }
    if (tot_1 == 5 && tot_0 == 9 && num == 1) {
        ++ans; // 记录合法方案数
    }
}

自己补充

二进制法适用范围:

  1. 一定的范围,int 的范围是 2 32 ^{32} - 1,再大遍历耗时太久,也就没意义了
  2. 两种对应状态,比如店与花、选取与不选取、买与不买、召唤与不召唤

二进制法通解过程:

  1. 确定状态,0 1 分别表示什么状态
  2. 确定枚举范围,即外部循环是什么
  3. 确定目标所求,即一次外部循环哪些需要求
  4. 确定子集范围,即内部循环是什么
  5. 确定具体所求,即一次内部循环哪些量需要改变
  6. 确定解,即什么样的遍历效果是我们所求,需要哪些限定条件

二进制法通解:

// 枚举范围
for(int i=0;i<(1 << n);i++){
	// 列出变化变量
    int ...;
    // 遍历每一位
	for(int j=0;j<n;j++){
	    if(i & (1 << j)){
	       // 如果满足状态,遍历随着变化
	    }
	}
	//当遍历完一种情况,累积量
	if(...)
		...;
}

猜你喜欢

转载自blog.csdn.net/liyuanyue2017/article/details/86523697