状态压缩中枚举子集
状态压缩中枚举子集
前言
在状态压缩中,有时会遇到集合间转移的题目,而如何找到某个集合的所有子集,成为了解决问题的关键。
正文
首先,对于一个集合 \(s\),我们可以通过它转换为二进制数后的 \(0\) 和 \(1\) 来表示一些元素的状态。如:选择和未选,经过与未经过。下文的集合均指其压缩后的二进制数。很显然,对于 \(s\) 的子集,它的大小不会超过 \(s\)。因为超过以后,意味着一定有一个不在 \(s\) 中的 \(1\)。所以,我们的枚举范围就缩小到所有小于等于 \(s\) 的数。假设我们枚举到了集合 \(x\),判断 \(x\) 是否为 \(s\) 的一个子集最简单也是最暴力的方法:枚举 \(x\) 每一位,看是否有不在 \(s\) 中的元素。
bool is_subset(int s, int x){
for(int i = 0; i<8; i++){
if(((s>>i)&1)==0&&((x>>i)&1)==1){
return 0;
}
}
return 1;
}
但这样有个显然的问题,就是太慢了。有没有快一点的方法判断是否为子集呢?
答案是判断一下 \(x\) & \(s\) 是否等于 \(x\) 即可。为什么这样是对的呢?因为与操作可以将 \(x\) 中所有在 \(s\) 中为 \(0\) 的位置上的 \(1\) 抹去,而为 \(1\) 的位置上不变。
那么有
bool is_subset(int s, int x){
return (x&s)==x;
}
好,现在我们有了快速判断子集的方法!那么,我们以 \(s = 11010\) 为例,看一看枚举的效果——
00011010
00011000
00010010
00010000
00001010
00001000
00000010
00000000
怎么样,是不是不重不漏~
emm,但是你还是感觉太慢了。如果 \(s\) 太大,那枚举的次数可是 \(2^n\) 级别的!
我们发现,当我们枚举时,有很多冗余的状态——
->00011010
00011001
->00011000
00010111
00010110
00010101
00010100
00010011
->00010010
00010001
->00010000
00001111
00001110
00001101
00001100
00001011
->00001010
00001001
->00001000
00000111
00000110
00000101
00000100
00000011
->00000010
00000001
->00000000
//箭头指出的是合法子集
我们发现,对于每一个合法的 \(x\),它的下一个合法子集(设为 \(y\) ),总是小于 \(x\),而中间的所有状态(设为 \(q\))都满足 \(y < q < x\),那我们不妨直接去生成这个 \(y\)。于是有 \(y = s\) & \((x-1)\)。
为什么这样是正确的呢?因为减去 \(1\) 会让 \(x\) 最后一个 \(1\) 变为 \(0\),而它之后的所有位变成 \(1\)。与 \(s\) 做与运算后,相当于消去最后一个 \(1\),然后再去枚举这个 \(1\) 之后的二进制数的子集。
可以发现,这样枚举也是不重不漏的。
void find_subset(int s){
int tmp = s;
print(s);
while(tmp){
tmp = (tmp-1)&s;
print(tmp);
}
}
输出如下——
00011010
00011000
00010010
00010000
00001010
00001000
00000010
00000000
这样子,枚举的复杂度就变为 \(2^k-1\) 了,其中 \(k\) 是集合中元素的数量。
#include<bits/stdc++.h>
using namespace std;
void print(int x){
for(int i = 7; i>=0; i--){
printf("%d", (x>>i)&1);
}
putchar('\n');
}
bool is_subset(int s, int x){
return (x&s)==x;
}
//bool is_subset(int s, int x){
// for(int i = 0; i<8; i++){
// if(((s>>i)&1)==0&&((x>>i)&1)==1){
// return 0;
// }
// }
// return 1;
//}
void find_subset(int s){
int tmp = s;
print(s);
while(tmp){
tmp = (tmp-1)&s;
print(tmp);
}
}
int main(){
int x = 0b11010;
find_subset(x);
return 0;
}