状态压缩DP前提知识:位运算

状态压缩DP前提知识:位运算

感谢B站一个大佬 :一俩三四五

简述状态压缩

状态压缩就是说,可以将集合以二进制排列的形式存储,这样可以减少其复杂度。

因为二进制就是0和1的排列,所以我们很方便的将数字映射到一个二进制数上。看图

在这里插入图片描述

假设一个集合(数学意义上的集合,即没有相同元素),例如图中的set={ 1,3,4,6} ,如果用一个二进制数来表示,那么就像下面的a一样,在set元素的地方为1,其余位置为0,那么这个a就表示了这个集合,也就是说,一个集合被我们压缩成了一个数字,而且这个数字虽然是以二进制形式的方式存储,但是我们在后续的计算中用十进制就可以(而且绝大多数情况就是只用十进制),一个数。那么这张图里,一个集合{1,3,4,6}被压缩成了一个数91来表示。

那么既然这样表示了,我们要知道对于集合有很多的操作,比如我们要加一个数放到集合中,从集合中删掉一个数等等,那对于这样明确表示的一个十进制数,我们该怎么处理呢?

这就需要利用位运算的知识了。

位运算符号

 &    |    ^   <<   >>

这几个基本的运算符是必须要掌握的,为了不用再去查资料,我在这里大体写一下。

&: 按位与,如果两个相应的二进制位都为1,则该位结果为1,否则为0;
|: 按位或,两个相应的二进制位只要有一个1,结果就为1;
^: 按位异或,如果两个二进制位相同则为0,不同则为1;
<<: 左移,将一个数的二进制全部左移N位,右侧补0;
>>: 右移,将一个数的二进制全部右移N位,右侧舍弃,无符号数左侧补0;

OK,知道了这些基本运算符之后,我们开始了解一下,在状态压缩DP问题中需要的位运算知识。

状态压缩中所需的基本知识

int A;  //set,一个数代表一个集合
int c;	//element,集合中的一个数

A|=1<<c; /*insert(c) 将c加入集合中。我们要把c这个数加到集合中,
也就是将集合中第c个位置设为1,那么就把1向左移c位,然后与集合A按位或,
根据按位或的计算,如果第c位是0,那么正好变为1,如果是1,
那么说明原集合中已有c这个元素,不用再加入,而其他位该是1的还是1,
该是0的还是0。*/

A&=~(1<<c);/*erase c 将c移除集合。我们看,先是1向左移c位,
那么就是在c的位置为1,然后再取反,那么就是除了c的位置,其余都是1,
然后再和A按位与,结果就是,其余位置是0的还是0,是1的还是1,
但是c的位置是0&1,结果是0,那么就把c去除了。*/

A^=1<<c;/*erase c 。这个是明确知道c这个元素已经存在于集合中,
上面那个即使不知道集合中是否存在元素c也依然可以使用。 
而这个也更简单了。 异或就是不同为1,相同为0,那么左移了c位之后,
两个1相异或就是0了,所以去除。*/

a&(-a); /*lowbit of A 。这个是非常经典的操作,
就是取集合A中最小的元素,返回的不是1的位置,
而是确切的最小的元素的十进制数。在下面会用图具体解释*/

A=0;//empty set 清空集合,没有问题

A|B;//union 取并集,没有问题

A&B;//intersection 取交集,没有问题

int size=15; //size of set 建立一个大小为15的集合

int ALL=(1<<size)-1;/*universal set 求全集(也就是全为1),
就是讲1向左移size位,这时候是第十六位为1,其余位全为0,
所以我们再减1,这样就变成了15个1
*/
ALL^A; /*complementary set of A, 求全集里A的补集,
也是用异或的性质,和全集不同的为1,就是A的补集*/

(A&B)==B; /*B is A's subset B是A的子集。
如果A和B相与之后还是B,那么B就是A的子集。*/

//enumerate the subset of ALL 枚举全集中的所有子集,因为是从小到大枚举,所以肯定可以枚举所有的子集。 
for(int i=0;i<=ALL;i++)
    ;

//enumerate the subset of A 枚举某一个集合中的所有子集。下面详解。
int subset=A;
do
{
    subset=(subset-1)&A;
}while(subset!=A);

//count the number of element in A
int cnt=0;
for(int i=0;i<size;i++)
{
    if(A&(1<<i)) cnt++;
}

for(int i=A;i;i>>=1)
    cnt+=i&1;
int lowbit(int x)
{
    return x&(-x);
}

在这里插入图片描述

如图中,如果a是01101011,那么进行lowbit(a)之后便得到1,如果是01100100,则得到100。

那么是如何实现的呢,我们就用01100100来看。首先我们要知道一个事情,就是负数在C语言中是以补码形式存储的,也就是说(-a)= 10011100. 那么补码我们知道是先在原码取反,取反之后我们先考虑低位开始第一个1,第一个1变为0,后面的全部变为1,但是取补码还要再加1,而正是加的这个1,让变成1的这些数重新变成了0,而原来第一个1又变成了1。那么这个1左边的高位没有受到任何影响,所以就是正常的取反。因此,a与a的补码按位与之后,高位的因为是相反的所以都变成了0,而低位只有那个1是没变的。因此得到了最小值100 .。 非常神奇。

int highbit(int x)
{
    int p=lowbit(x);
    while(p!=x) x-=p, p=lowbit(x);
    return p;
}

而对于求最高位就没有好的方法了。我们就依赖求最低位的方法,不断的将最低位减掉,当当前位和这个数字一样的时候,那就说明是最高位了。然后返回。(这个操作很少用到)

bool powerOf2(int x) //x is the power of 2判断一个数是不是2的幂.x不能为0
{
    return x&&!(x&(x-1));
}

在这里插入图片描述

我们就拿8来看,8的二进制是1000,8-1=7的二进制是0111,他俩相与肯定是0 。

那么我们更加一般的来看,如果任意给一个不是2的幂次的数,那么二进制中肯定有两个或以上的1对吧,就像图中那样,那么-1之后,只能对第一个1以及后面那些位产生影响,而不会影响高位,所以与之后肯定有1,那么只要不是1的值就是true。因此就可以用这个方式来判断。

int subset=A;
do
{
    subset=(subset-1)&A;
}while(subset!=A);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SLpL4jXV-1607263722242)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20201206215019746.png)]

首先明确一个概念,状态压缩中的子集是什么意思。

例如图中这个A是一个集合,它其实表示的是{0,1,3,5}这个集合对吧。那么根据数学所学的,它的子集个数应该为2的4次方个。那么对应到状态压缩中,例如一个子集是{0,1},那么他就应该是subset=000011. 对吧,子集{1,3,5}应该就是101010对吧。

好那么来看为什么这个循环可以找出所有的子集。如图中,我们先设i=A(此时i就是A,这个求子集可以求出集合A本身和空集,因为这俩也是它的子集)。那么我们就像图里画的那样来看,第一次减1与A相与,变成了101010,对应的{5,3,1},再减1与A相与,变成了101001,对应的{5,3,0},目前来看都是对的,等到了减1完变成100111时我们感觉好像有点不对劲,但是它要与A相与,那么就只会保留低位1变成了100011对于{5,1,0}。对吧,是不是很神奇。

那么如何证明这样去枚举为什么是正确的而且得到的子集是完整的呢?具体的数学证明还是有点麻烦的。如果感兴趣的可以自行去了解。

还有一个问题就是,我们如何求0到2的n次方中所有数(也就对应的集合)的子集的个数。

int cnt[0]=0;//空集没有元素
for(int i=0;i<pow(2,n-1);i++)
{
    cnt[i]=cnt[i>>1] + (i&1);
}

还有一个问题就是,我们如何求0到2的n次方中所有数(也就对应的集合)的子集的个数。

int cnt[0]=0;//空集没有元素
for(int i=0;i<pow(2,n-1);i++)
{
    cnt[i]=cnt[i>>1] + (i&1);
}

因为我们在算某个i的时候,其实比i小的都已经算过了,所以我们只需要把最后一位拿掉,也就是cnt[i>>1],这就是把最后一位以外的集合元素有多少个,再加上(i&1)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我们来看一个具体的例子。假设有一个长度为 n 的数组 A,其中每个元素都是 0 或 1,现在需要求出所有长度为 k 的子串中,元素为 1 的个数的最小值。 传统的动态规划方法需要使用二维数组来记录状态,时间复杂度为 O(nk),空间复杂度为 O(nk)。而使用状态压缩dp,我们可以将状态压缩为一个长度为 n 的二进制数 i,其中第 j 位为 1 表示 A[j] 在当前子串中出现了一次或多次,为 0 则表示没有出现。因此,我们只需要使用一个一维数组 f 来记录当前状态的最小值即可。 具体实现如下: ```python def min_ones_in_k_substrings(A, k): n = len(A) f = [float('inf')] * (1 << n) f[0] = 0 for i in range(n): for j in range(1 << i): if bin(j).count('1') == k: ones = bin(j & ((1 << i) - 1)).count('1') + A[i] f[j] = min(f[j], f[j & ~(1 << i)] + ones) return f[(1 << n) - 1] ``` 其中,f[i] 表示状态为 i 时的最小值,初始化为正无穷。在状态转移时,我们枚举当前状态的所有子集 j,如果 j 中的元素个数等于 k,则计算 j 中包含的所有元素为 1 的个数 ones,然后更新 f[j] 的值为 f[j] 和 f[j - {i}] + ones 中的较小值。其中,j - {i} 表示将 j 中的第 i 位(即 A[i] 对应的位置)置为 0。 最终,我们返回状态为全集时的最小值 f[(1 << n) - 1] 即可。由于状态总数为 2^n,因此时间复杂度为 O(n^22^n),空间复杂度为 O(2^n)。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值