在稍微有难度一点的动态规划或者其他问题中,经常会使用用整数表示集合。在程序中表示集合的方法有很多种,当元素数比较少时,像这样用二进制码来表示比较方便。集合
{
0
,
1
,
2
,
.
.
.
,
n
−
1
}
\{0,1,2,...,n-1\}
{0,1,2,...,n−1}的子集
S
S
S可以用如下方式编码。
f
(
S
)
=
∑
i
∈
S
2
i
f(S)=\sum_{i\in S}2^i
f(S)=∑i∈S2i,即
{
0
,
1
,
10
,
100
,
.
.
.
,
100000000...
}
\{0,1,10,100,...,100000000...\}
{0,1,10,100,...,100000000...}存在即1,其余位为0。
这样表示之后,一些集合运算可以通过如下方式高效的进行。
- 空集 ∅ \emptyset ∅————————————————————————0
- 只含有第 i i i个元素的集合 { i } \{i\} {i}:———————————— 1 < < i 1<<i 1<<i
- 含有全部 n n n个元素的集合 { 0 , 1 , . . . , n − 1 } \{0,1,...,n-1\} {0,1,...,n−1}:———— ( 1 < < n ) − 1 (1<<n)-1 (1<<n)−1
- 判断第
i
i
i个元素是否属于集合
S
S
S:——————————
S
>
>
i
&
1
S>>i \&1
S>>i&1
(也就是将 S S S的第 i i i位移到了个位,这样如果第 i i i存在,那么个位就会为1)。 - 向集合中加入第 i i i个元素 S ∪ { i } S\cup\{i\} S∪{i}—————————— S ∣ 1 < < i S|1<<i S∣1<<i
- 从集合中去除第
i
i
i个元素
S
/
{
i
}
S/ \{i\}
S/{i}——————————
S
&
!
(
1
<
<
i
)
S\& !(1<<i)
S&!(1<<i),
( 1 < < i ) (1<<i) (1<<i)是 i i i元素的编码,将其取非,得到第 i i i为0,其余位为1的集合,与运算即将第 i i i位置零。 - 集合 S S S和 T T T的并集———————————————— S ∣ T S|T S∣T
- 集合 S S S和 T T T的交集———————————————— S & T S\&T S&T
-
S
S
S最低位的1独立出来的值————————————
S
&
(
−
S
)
S\&(-S)
S&(−S)。
这是由于计算机中负数采取二进制补码形式表示, − x -x −x其实对应于 ( ! x ) + 1 (!x)+1 (!x)+1(按位取反,末尾加1),也就是最低位1不变,高位全部取反。
枚举集合的所有子集
按照下列方式进行枚举的话, S S S就会从空集开始,然后按照 { 0 } , { 1 } , { 0 , 1 } . . . \{0\},\{1\},\{0,1\}... {0},{1},{0,1}...的升序枚举出来。
for(int s = 0; s < 1<<n; s++){
//对s的操作
}
枚举子集的子集
上面枚举子集采取了升序枚举,不断+1的手法,那是因为我们知道,所有 s < 1 < < n s<1<<n s<1<<n肯定都是 S S S的子集。但是如果我们现在有一个子集,比如 100101 100101 100101,要枚举他的子集,有一个难点
- +1之后不一定就是他的子集,比如100110就不是他的子集。这个问题我们可以采用集合的交集运算,将它改为子集。
- 但是如果对100110而言,+1之后100111,取交集依然是100110,没有变化,此时我们采取降序枚举的方式,每次给-1然后和原集合取交集。
int sub = 7, sup = sub;
do {
//对子集的处理
sub = (sub - 1)⊃
} while (sub != sup);//处理完0之后,会有-1&sup=sup,-1的补码全为1