关于枚举子集优化
前言:为什么要使用枚举子集优化?
在状态压缩的题目中,我们常常会遇到集合之间转移的题目,而如何快速找到某个集合的子集就成为了我们重点关注的算法过程。
暴力枚举子集:
很显然,对于 \(S\) 的子集,它的大小不会超过 \(S\) 。所以我们只需要枚举小于等于 \(S\) 的二进制数,假设我们枚举到了子集 \(sub\) ,很简单的,我们只需要判断 \(sub\) 的每一个二进制位是不是都小于等于 \(S\) 的二进制位就可以了,但是这样太慢了,上界复杂度达到了 \(O(4^n \cdot n)\) 。(假设我们最大的集合大小为\(n\),我们从大集合的子集一步一步转移,每次选定一个子集,又需要枚举子集的子集才可以转移)
探索优化过程:
事实上,上面的判断每一位是否超过 \(S\) 的每一位的比较方法,可以直接等价于判断 \(x\&S=x\) 是否成立就好了,这个用位运算的基本定义很好推出来,复杂度优化到理论上界 \(O(4^n)\) 但是你发现还是不够优秀。其实这是因为,一个子集 \(S\) ,他子集的子集 \(subset\) 的个数最多只有 \(2^{|S|}\) 个,而刚才的方法是直接枚举到大概上界 \(2^n\) 次。
那么我们想,是否存在一种方法,不需要判断,可以直接枚举子集呢?(有的兄弟,有的),其实我们如果得到了一个 \(S\) 的子集 \(subset\) ,下一个子集一定是 \((subset-1)\&S\) (如果我们从大到小枚举子集的话)。证明的话就是等价于证明区间 \([(subset-1)\&S+1,subset- 1]\) 中不存在其它 \(S\) 的子集。非常简单,只需要形式化找到 \(subset\) 的最低的为\(1\)的为,再通过子集的定义与二进制的计算就可以证明,当然了,若没有这样的位就证明 \(subset\) 为空集,即:值为\(0\)。
为什么上面这个过程的复杂度是 \(O(3^n)\) ?这个可以用二项式定理进行计算:枚举单个\(S\)的子集的复杂度是 \(O(2^{|S|})\)的,所以枚举全集(大小为\(n\))的每个子集(\(S\))的子集(\(subset\))的复杂度是:
的。
模板代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
void print(int x){
for(int i = 7;i >= 0;--i){
printf("%d", (x >> i) & 1);
}
putchar('\n');
}
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;
}

浙公网安备 33010602011771号