标签:sim pre display box i++ header .com 增加 spl
如果通过【二进制】按位运算的方式来进行【集合】操作,则会给算法带来简洁、快速等特点。是【状压DP】中常用的【状态】表示形式。
- 如果【集合】元素的取值只有【两种可能】最好,如果有多种可能,可能需要多【位】来表示【一个元素】
\[ \{ 2,4,5 \} \rightarrow 110\overbrace{\fbox{1}}^{包含 2 }0\underbrace{ \fbox{0}}_{不包含0} \tag{1}\]
集合 | c++ 代码 | 备注 |
---|---|---|
\(E\) :全集 | $(1<< n) -1 $ | $ 2^n -1 = \underbrace{(111\dots111)}_{共n位} $ |
\(\{k\}:第k个元素\) | $ 1 << k$ | \(00\dots\overbrace{\fbox{1}}^{第k位 }\dots00\) |
$\sim S = E - S $ :补集 | $ e & (\sim s) \(|绝对补集 | |\)\phi ; 或者 {} $ :空集 | $0 $ |
集合操作 | c++代码 | 备注 |
---|---|---|
\(S+\{k\}\) | $ s | (1 << k)\(|把第k位设置为 1 | |\)S-{k}$ |
集合操作 | c++代码 | 备注 |
---|---|---|
$[k \in S] $ | $ (s \gg k ) & 1\(| S是否包含k ? | |\) lowbit(S)$ | $ s & (-s)$ |
$ | S | =\sum_{k\in S} 1 = \sum_{k=0}^{n-1}[k\in S]$ |
\([|S|=1]\) | $ lowbit(s)==s \(| S 是否只有1个元素 ? | |\)S \oplus T$ | $ s \wedge t \(| 【异或】操作,参见下面的解释 | |\)S \oplus {k}$ |
集合的异或
: 异或也称为【对称差】
\[S \oplus T = (S \cup T )-(S \cap T) = (S-T) \cup (T-S)\]
文氏图:
[axorb.png-2.2kB][1]
[1]: http://static.zybuluo.com/headchen/y9pkjazwkfcgpjij5kp83zp3/axorb.png
代码为 s ^ t,所需要把第k位取反,则 代码为 s ^ (1 << k) 即可。
【枚举子集】的返回结果是一个包含了所有【子集】的集合,然后可以遍历这个【集合】执行某个操作,一般情况下是“边枚举,边执行”,这样可以省略空间,但不方便数学上的表示。
数学上称为【幂集】,表示为\[ P(S) = \{ x | x ? S \} =\{ \phi, \{1\},
\{2\}, \{3\}, \{1,2\}, \{1,3\}, \{2,3\}, \{1,2,3\} \dots\}\]
- 注意:一定不能忽略【空集】
- 因为每一个元素均有“选择”“不选择”两种可能,根据【乘法原理】,那么选择的方法就有 \(2^n\) 种(包括空集),也就是说\(|P(S)| = 2^k\)
在实际的代码中:
for(int i=0;i<(1<<n);i++)
{
DoSomeThing(i); //这里i代表“压缩为i”的集合。
}
\[P(S,j) = \{j\} \cup P(S,j+1)\;\bigcup \; P(S,j+1)\]
其含义是:以第j个元素是否【包含】进行分类枚举,可以看出和【组合计数】中的递推式很像。对这个数学公式进行递归求解,就是【回溯】枚举子集:
//n:原集合中有n个元素,begin:从第j开始,s表示 结果集合中的“一个子集”
void Subset(int n, int begin, vector<int> &chosen)
{
if (begin > n)
return;
chosen.push_back(begin); //选择begin ,产生了一个新的子集,调用 dosomething
doSomething(chosen);
Subset(n, begin + 1, chosen); //递归调用
chosen.pop_back(); //恢复现场
Subset(n, begin + 1, chosen); //不选择,直接调用
}
void Subset(int n)
{
vector<int> chosen;
doSomething(vector); //处理空集
Subset(n, 1, chosen);
}
令 \(P(S,j)\) 表示 “从第j个元素开始” 的所有的元素的【子集】:
\[P(S,j) = \bigcup_{k=j}^n \big( \{j\} \cup P(S,k+1) \big) \]
其含义是:从j开始,给子集“增加一个元素,从而产生新的子集,有多少种方法?”的形式进行分类枚举,对这个数学公式进行递归求解,就是【回溯】枚举子集:
void doSomething(vector<int> &chosen)
{
}
void Subset(int n, int begin, vector<int> &chosen)
{
//【空集】也算一个,每次调用都产生新的【子集】
doSomething(chosen);
if (begin > n)
return;
//从begin 开始,增加一个元素有多少种方法?
for (int i = begin; i<=n; i++)
{
chosen.push_back(i);
Subset(n, i + 1, chosen); //递归调用
chosen.pop_back(); //恢复现场
}
}
void Subset(int n)
{
vector<int> chosen; //不用特意处理【空集】
Subset(n, 1, chosen);
}
【组合】其实就是固定数目的【子集】,所以算法和【枚举子集】基本一致,控制子集的数目就可以了。
令 \(P(n,m,j)\) 表示 “从第j个元素开始” 的所有的元素的【子集】,【子集】的数目必须为m:
\[P(n,m,j) = \{j\} \cup P(n,m-1,j+1)\;\bigcup \; P(n,m,j+1) \tag{1}\]
//n:集合个数,m:【还需要选几个】 begin: 从第几个开始选择 chosen:返回的答案
void Combination(int n, int m, int begin, vector<int> &chosen)
{
if ((n - begin + 1) < m) //剩下的不够选了,剪枝了
return;
//选择begin
chosen.push_back(begin);
if (chosen.size() == m)
DoSomething(chosen);
else
Combination(n, m - 1, begin + 1, chosen);
chosen.pop_back(); //恢复现场,因为chosen是【引用】
//不选择
Combination(n, m, begin + 1, chosen);
}
void Combination(int n, int m)
{
vector<int> chosen;
DoSomething(chosen); //注意:处理【空集】
Combination(n, m, 1, chosen);
}
也可以如下定义:
\[P(n,m,j) = \bigcup_{k=j}^n \big( \{j\} \cup P(n,m-1,k+1) \big) \]
//n:集合个数,m:【还需要选几个】 begin: 从第几个开始选择 chosen:返回的答案
void Combination(int n, int m, int begin, vector<int> &chosen)
{
if ((n - begin + 1) < m)
return;
if (chosen.size() == m)
{
DoSomething(chosen);
return;
}
for (int i = begin; i <= n; i++)
{
chosen.push_back(i);
Combination(n, m - 1, i + 1, chosen);
chosen.pop_back();
}
}
void Combination(int n, int m)
{
vector<int> chosen;
Combination(n, m, 1, chosen);
}
标签:sim pre display box i++ header .com 增加 spl
原文地址:https://www.cnblogs.com/headchen/p/9341210.html