位运算(偏向状态压缩)的骚操作
2019-08-05: 修改笔误,完善内容。
基操勿六皆坐
集合${0,1,\cdots,n-1}$的子集$S$可用如下方法编码成整数:
$$f(S)=\sum_{i\in S}2^i$$
一些集合的运算可以对应地写成如下方式。
-
空集$\varnothing$:
0
-
只含有第$i$个元素的集合${i}$:
1<<i
-
${0,1,\cdots,n-1}$:
(1<<n)-1
-
判断第$i$个元素是否属于集合$S$:
if(S>>i&1)
-
向集合中加入第$i$个元素$S\cup{i}$:
S|1<<i
-
从集合中去除第$i$个元素$S\setminus {i}$:
S&~(1<<i)
-
并集$S\cup T$:
S|T
-
交集$S\cup T$:
S&T
lowbit
该技巧用于取一个二进制数的从右往左第一个非0位所表示的10进制数。
对于int
类型来说,~x = -x-1
原理:对于一个正数的整型数据,计算机在存储对应的负数时,会将该正数按位取反后(反码)再加1(补码),例如:
x = 0100 -x = 1100 x&-x = 0100
显然对于任意一个正数x,除从右往左第一个非0位及其右面的位与-x对应位相同以外,其他位都相反,利用这个性质,可以快速取出一个二进制数的从右往左第一个非0位所表示的10进制数。
代码如下:
|
|
枚举$S$中的所有子集
|
|
每次对当前生成的子集减1,也就是将S_
最低位的1变成0,更低位的所有0变成1,如1100-1 = 1011,即每次将S_
中元素序号最小的去掉,再将集合S
中比前述去掉的元素的序号更小的所有元素加入S_
,直到S_=0
再减1,退出循环。
还有这种操作?
枚举${0,1,\cdots,n-1}$中所有大小为$k$的子集
|
|
按照字典序的话,最小的子集是(1<<k)-1
,所以用它作为初始值。现在我们求出comb
其后的二进制码。例如0101110之后的是0110011,0111110之后的是1001111。下面是求出comb
下一个二进制码的方法。
-
求出最低位的1开始的连续的1的区间(0101110->0001110)
-
将这一区间全部变为0, 并将区间左侧的那个0变为1(0101110-0110000)
-
将第1步里取出的区间右移,直到剩下的1的个数减少了1个(0001110->0000011)
-
将第2步和第3步的结果按位取或(0110000|0000011=0110011)
用lowbit将最低位的1取出后,设它为x
。那么通过计算y=comb+x
,就将comb
从最低位的1开始的连续的1都置0了。我们来比较一下~y
和comb
。在comb
中加上x
后没有变化的位,在~y
中全都取相反的值。而最低位1开始的连续区间在~y
中依然是1,区间左侧的那个0在~y
中也依然是0。于是通过计算z=comb&~y
就得到了最低位1开始的连续区间。比如如果comb=0101110
则x=0000010
,y=0110000
,z=0001110
。
同时,y
也恰好是第2步要求的值。那么首先将z
不断右移,直到最低位为1,这通过计算z/x
即可完成。这样再将z/x
右移一位就得到了第3步要求的值。这样我们就求出了comb
之后的下一个二进制列。因为是从n
个元素中进行选择,所以comb
的值不能大于1<<n
。如此一来,就完成了大小为k
的所有子集的枚举。
枚举${0,1,\cdots,n-1}$中所有不包含相邻元素的集合
不包含相邻元素的集合,简单理解即为表示集合的二进制数中不包含相邻的1。
|
|
注:该结果的计数为$f(n)=F_{n+2}$,其中${F_{n}}(F_1=1,F_2=1)$为斐波那契数列。2
证明如下:
当$n=0$时,显然$f(0)=1$,
当$n=1$时,即枚举${0}$中所有不包含相邻元素的集合,即$f(1)=2$,
对于$n>1$,当最低位为0时,只需次低位及其左边的二进制位不包含相邻元素,方案数为$f(n-1)$;当最低位为1时,次低位必须为0,其左边的二进制位不包含相邻元素,方案数为$f(n-2)$。故总方案数为$f(n)=f(n-1)+f(n-2)$。
参考该结论,可以在线性时间内算出任意集合$S$的不包含相邻元素的子集的计数问题。
妙啊!妙啊!妙啊!
计算$S$中有多少元素
|
|
集合$S$中的最小元素编号
|
|
-
挑战程序设计竞赛(第2版). 秋叶拓哉等. ↩︎
-
Fibonacci number. Wikipedia. https://en.wikipedia.org/wiki/Fibonacci_number ↩︎