二进制枚举子集的技巧详解

代码部分

对于一个集合 x x x,枚举其所有子集,则有:

for(int i = x;i;i = ((i - 1) & x)){
	//TODO
}

其中,i是枚举变量,i的每个取值都对应着 x x x的一个子集。TODO部分是对于当前枚举到的子集的进一步使用、处理。

讲解部分

先来看两个边界状态:

起始边界:

很明显是i = x,表示从原集合本身开始枚举。该状态是合法状态,可以被取到。

终止边界

当前算法中,我们认为终止边界状态为非法状态,即空集不为任何集合的子集(数学意义下空集为任意集合的子集),该状态不可能被取到(原因:当i = ((i - 1) & x)语句将i更新至0时,for循环跳出,不再执行循环体,故不可能被取到)

再来看看迭代部分:

迭代部分

i = ((i - 1) & x)

为了方便理解,我们用一个实例手工模拟迭代部分,来观察它的工作原理:

基本起始状态(当前状态为子集)
1 0 1 0 1
减一
1 0 1 0 0
x x x按位与(当前状态为子集)
1 0 1 0 0
减一
1 0 0 1 1

到这里,我们先暂停。请仔细观察1 0 1 0 01 0 0 1 1之间(在形式或对应位上)的关系。

数学意义下,我们知道后者由前者-1后得到;但我们在形式或对应位上观察发现:减一操作,就是把从低位(右侧)向高位数第一个1改为0,再把这一位右侧的所有值改为1。

我们继续模拟:

x x x按位与(当前状态为子集)
1 0 0 0 1

到这里暂停一下。观察1 0 0 1 11 0 0 0 1之间的关系。(提示:思考按位与作用在每一位上的效果,不必思考数学意义下按位与的含义)

在形式上观察发现,(从右向左数)第二位由1变为了0

Q Q Q :为什么算法设计师认定,这个1是应该且必须被去掉的呢?

A A A :因为这个被去掉的1就不在 x x x中。

首先我们知道:我们的变量i枚举的是x的子集,也就是 i ∈ x i \in x ix始终成立。这个被去掉的1是不再x中的,所以我们需要将它去掉。

Q Q Q :但是按位与是作用在所有二进制位上的啊?在筛去刚才减一操作中右侧不应该存在的1时,不会对左侧造成影响吗?

A A A :其实不会。

其实在枚举中,我们能发现一个规律:循环变量i其实是在倒序枚举 x x x的所有子集。

理解这一点其实很简单。我们每次减1,在形式上看来其实是找到从右向左第一个1,变成0,这样只有右侧的所有可能都枚举完了,才会改变偏左的部分,继续枚举

因为最开始有i = x存在,而且上面我们说,我们是倒序枚举,右边先改,左侧暂时不变。而右侧原本全是1,所以按位与后可以使右侧成为x的“子集”(或者说将右侧某些在x中不存在的1改为0),这就成了x的一个子集。

再往后计算时,就找到当前最右边的1,以它为基准继续操作就好了。

其实到这里,我们就发现了这个迭代部分的工作原理。

其实,这感觉有点像在一位数组上用dfs算法枚举所有情况,对于每一层,都有01两种状态,对于每个状态进入下一层dfs进一步处理。

好了,今天的博客就到这里了。如果你感兴趣,可以将上边的例子自己推下去,看看是不是这样的。

感谢阅读。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值