Loading

关于枚举子集优化

前言:为什么要使用枚举子集优化?

在状态压缩的题目中,我们常常会遇到集合之间转移的题目,而如何快速找到某个集合的子集就成为了我们重点关注的算法过程。

暴力枚举子集:

很显然,对于 \(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\))的复杂度是:

\[O\left(\sum_{i=0}^{n}\left(\dbinom{n}{i}\cdot2^i\right)\right)=O\left((1+2)^n\right)=O(3^n) \]

的。

模板代码:

#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;
}
posted @ 2025-03-05 08:25  AxB_Thomas  阅读(110)  评论(0)    收藏  举报
Title