基础状压dp

一、位运算

计算机中数字以二进制的方法存储,位运算是针对二进制进行的一种速度极快的基础运算。C++里一个int有4个字节也就是32位二进制数,其中最高位是符号位,1表示负数,0表示正数。除此以外,char是1个字节8位,long long是8个字节64位,bool是1个字节1位,unsigned类型的字节和位数不变,只是最高位不再是符号位,所以可以表示更大的数字。

C++的位运算包括左移(<<)、右移(>>)、与(&)、或(|)、取反(~)、异或(^)。左移和右移分别指将该二进制数整体向左(右)移动若干位数(在算法竞赛所使用的编译器中,左移和右移都不会移动符号位,称为算术左(右)移)。下面给出示意表。

x(char类型)

x<<1

x<<2

x>>1

x>>2

01110001

01100010

01000100

00111000

00011100

11000001

10000010

10000100

10100000

10010000

另外,左移右移可以用来加速一些运算,因为x<<y的值等于x(2y),x>>y的值等于x÷(2y)。所以比如2*x可以写成(x<<1),x/2可以写成(x>>1),pow(2,x)可以写成(1<<x)。由于位运算是最快的运算方式,这些写法能够大大减小程序的常数。

         与、或、非、异或的对照表如下:

A

B

~A

A&B

A|B

A^B

0

0

1

0

0

0

0

1

1

0

1

1

1

0

0

0

1

1

1

1

0

1

1

0

         很重要的一点:位运算的优先级很迷,使用时一定要加足够多的括号。

 

【小练习 设计位运算】

要得到下表的结果,需要怎样的位运算?

第一个数a

0

0

1

1

第二个数b

0

1

0

1

结果

1

0

1

0

第一个数a

0

0

1

1

第二个数b

0

1

0

1

结果

1

1

0

1

 

第一个数a

0

0

1

1

第二个数b

0

1

0

1

结果

1

0

1

1

第一个数a

0

0

1

1

第二个数b

0

1

0

1

结果

0

0

1

0

 

二、状态压缩

    一个int有32个0或者1,除去符号位可以被当成一个bool数组来用。(需要记住的是,对于整数的二进制表示我们一般以最右边的位为第0位)bool数组支持查询并修改数组中某一个变量的值,如果使用位运算的话也可以做到这一点。下面是bool a[31]和int a两个东西相关操作的对照表:

 

查询第i位

把第i位置成1

把第i位置成0

把第i位取反

bool a[31]

a[i]

a[i]=1

a[i]=0

a[i]=!a[i]

int a

a&(1<<i)

a=a|(1<<i)

a&=(~(1<<i))

a^=(1<<i)

    这种做法俗称二进制压位,如果仅仅只是把它当成bool数组来用,也可以节省相当一部分的空间。如果需要更大的bool数组可以利用STL自带的容器bitset,它能够实现几万位二进制数的位运算并且节省空间。

 

    三、状态压缩dp

    dp的时候需要设计状态,但是有的状态会很复杂。对于复杂的状态,也许就不能再像以前那样用一个i简单表示。或许这个状态表示一个有n(n<=16)个元素的集合,甚至包含了每一个元素的情况,你总不能开16维数组吧!为了应对这种情况,我们可以利用状态压缩和位运算,让一个数字表示一个集合。状压dp也需要三个步骤:

设计状态:至少有一维是用一个数字表示一个集合。

状态转移:考察每一个决策对集合的影响,经常使用位运算进行转移。

边界条件:当集合为空或者说只有一个元素之类的。

特别注意:状压是指数级算法,所以适合状压的题往往有一个维度的数字很小(比如n<=12,n<=16什么的)。

 

【例1 互不侵犯(弱化版)】

在给定大小的棋盘中放置国王(国际象棋),使得国王无法互相攻击,求放法数目。棋盘大小<=9*9。

 

分析:这个题的状态可以设计为“当前这一行上有哪些位置放了国王”。你可以设计这样的dp数组:dp[a1][a2][a3]...[a9][i],其中aj=1或者0,表示第i行第j列上有或没有国王。dp值就表示这种时候的方案个数。但是10维数组实在是太讨厌了!我们发现前面那些不是1就是0的维度可以拼在一起成为一个9位的二进制数,所以可以用一个数字S表示第i行上国王的状态,S的第j位是1,表示有国王,0则没有国王。

接下来考虑状态转移。首先国王可以攻击自己左边或者右边,所以如果S里有两个挨着的1(也就是说(S&(S<<1))!=0),就会直接出局。除此以外,第i行也和上一行有关:设上一行的状态是T,如果同一列上有国王((S&T)!=0),或者说相邻列上都有国王((S&(T<<1))||(S&(T>>1))),就不能从dp[T][i-1]转移。所以总的dp方程就是dp[S][i]=∑dp[T][i-1],其中S&(S<<1)==0而且(S&T)==0而且(S&(T<<1))==0而且(S&(T>>1))==0。枚举i,S,T进行转移,时间复杂度是n4n

 

【例2 玉米田】

一块农场,有一些土地能种,有一些不能。求种地方案数,使得没有两块土地相邻(有公共边称为相邻)。土地大小<=12*12。

 

    分析:跟上一题没有什么区别嘛。同样是设计dp[S][i]表示第i行的状态是S,前i行的方案个数。但是不同的是输入数据还描述了哪些土地可以种哪些不能。用a[i]表示第i行能种或者不能种的情况,0表示能,1表示不能,这样的话S必须满足(S&a[i])==0,也要满足和上一题一样的(S&(S<<1))==0,。考虑上一行的状态T,发现题目的“没有两块土地相邻”只需要(S&T)==0就可以。所以总的dp方程和上一题也几乎一样:

dp[S][i]=∑dp[T][i-1],其中S&(S<<1)==0而且(S&T)==0而且(S&a[i])==0。

有的人可能不明白枚举S或者T的顺序,实际上非常简单:只需要从0到(1<<n)-1一个个地枚举就可以了。因为实际上右边的dp[T][i-1]在执行赋值之前肯定是已经被算出来了的,可以直接转移。

 

【例3 旅行商】

    有n个城市,编号为0~n-1,两两之间均有道路相连。给出每两个城市i和j之间的道路长度相连。给出每两个城市i和j之间的道路长度Li,j,求一条经过每个城市一次且仅一次,最后回到起点的路线,使得经过的道路总长度最短。n<=15

 

    分析:这是一道经典的NPC问题,不存在多项式时间内的解法。如果使用纯暴力的搜索,时间复杂度为n!,肯定会超时,所以要考虑状态压缩。但是这道题不像上面两道,状态很容易看出来。好在至少我们知道dp数组有一维表示当前所在的位置,另一维表示所有城市的状态。城市的状态无非就是“去过”和“没去过”两种,“去过”就是1,“没去过”就是0,所以dp数组就是dp[S][i],表示当前在第i个城市,这n个城市的状态是S。

    和上边的题一样,状态有很多限制条件。首先,我既然在第i个城市,那么第i个城市我肯定去过,所以(S&(1<<i))!=0。假设我是从j城市来到i城市的,那么我来之前的状态T等于(S-(1<<j)),而且我肯定也去过j城市,有(S&(1<<j))!=0。综合以上分析可以得出dp方程:

dp[S][i]=min{dp[S-(1<<j)][j]},其中(S&(1<<i))!=0且(S&(1<<j))!=0且i!=j。

    题目要求的是一个环,那么dp的边界从哪里找?我们不妨假设他从0号城市出发,最终回到0号城市,那初始状态就是dp[1][0]=0吗?很遗憾如果真的这样写,最终结果就是你给dp数组赋的初值。因为0号城市已经被你标记上了,“不能重复经过同一个城市”使得你再也回不去0号城市。所以初值应该是dp[1<<i][i]=dis[0][i],其中i表示0~n-1城市。这样就可以在n22n的复杂度内解决该问题。

posted @ 2022-03-18 14:49  chyuhoin  阅读(84)  评论(0)    收藏  举报