二进制集合运算
Posted headchen
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二进制集合运算相关的知识,希望对你有一定的参考价值。
二进制集合运算
如果通过【二进制】按位运算的方式来进行【集合】操作,则会给算法带来简洁、快速等特点。是【状压DP】中常用的【状态】表示形式。
- 如果【集合】元素的取值只有【两种可能】最好,如果有多种可能,可能需要多【位】来表示【一个元素】
- “最好”从0【位】开始而不是从【1】开始。
[ { 2,4,5 } ightarrow 110overbrace{fbox{1}}^{包含 2 }0underbrace{ fbox{0}}_{不包含0} ag{1}]
集合的表示
集合 | c++ 代码 | 备注 |
---|---|---|
(E) :全集 | $(1<< n) -1 $ | $ 2^n -1 = underbrace{(111dots111)}_{共n位} $ |
({k}:第k个元素) | $ 1 << k$ | (00dotsoverbrace{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_{kin S} 1 = sum_{k=0}^{n-1}[kin 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)
- 忽略【顺序】的枚举
令 (Bin(i)) 表示 i 的二进制压缩集合。则:
[P(S) = igcup_{i=0}^n Bin(i)]
在实际的代码中:
for(int i=0;i<(1<<n);i++)
{
DoSomeThing(i); //这里i代表“压缩为i”的集合。
}
递归枚举【子集】的方法(一)
- 在很多【搜索】算法中,枚举的顺序是“按照【回溯】算法”的顺序枚举,则必须用递归式来定义:
令 (P(S,j)) 表示 “从第j个元素开始” 的所有的元素的【子集】:
[P(S,j) = {j} cup P(S,j+1);igcup ; 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) = igcup_{k=j}^n ig( {j} cup P(S,k+1) ig) ]
其含义是:从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);igcup ; P(n,m,j+1) ag{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) = igcup_{k=j}^n ig( {j} cup P(n,m-1,k+1) ig) ]
//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);
}
以上是关于二进制集合运算的主要内容,如果未能解决你的问题,请参考以下文章