状压dp概念和使用技巧
1.概念
状压就是用一个N进制整数,表示一个状态。
比如:
1:有一个序列[A,B,C,D],序列中的每个元素可以拿或者不拿。
则一共有2^4种方式,可以用一个4位的2进制整数来表示每一种方式(状态)。
整数6[0110]表示拿了B,C
整数1[0001]表示拿了A
整数15[1111]表示都拿了
2:有一些人[a,b,c,d],每个人可以拿0,1,2个苹果。
则一共有3^4种方式,可以用一个4位的3进制数来表示每一种方式(状态)。
整数3表示的3进制数为[0010],表示只有b拿了一个苹果。
整数5表示的3进制数为[0012],表示b拿了一个苹果,a拿了2个苹果。
2.状态表示和计算
在使用2进制表示状态的时候,因为计算机底层表示一个整数就用的2进制,所以很容易通过位运算得知每一位的数是多少。
但是如果使用N进制表示状态,并且N不是2,4,8…,比如3进制,5进制;此时要计算该整数每一位的数是多少就不能使用位运算了。
我们知道一个n位的m进制数,表示的十进制整数S为:
S
=
x
n
−
1
∗
m
n
−
1
+
x
n
−
2
∗
m
n
−
2
+
.
.
.
+
x
0
∗
m
0
S = x_{n-1}*m^{n-1} + x_{n-2}*m^{n-2}+...+x_0*m^{0}
S=xn−1∗mn−1+xn−2∗mn−2+...+x0∗m0
因此想要计算的整数S的m进制表示中的第i位的数是多少,可以使用下面的公式:
x
i
=
(
S
%
m
i
)
/
m
i
−
1
x_i = (S\%m^i)/m^{i-1}
xi=(S%mi)/mi−1
比如上面的示例2中的整数5:
x0 = (5%3)/1 = 2
x1 = (5%9)/3 = 1
3.使用技巧
1.一般可以使用回溯法求解的问题,并且状态不多时,就可以考虑使用状压dp;对于某些图问题,通常会通过按行或者按列进行dp,每一行或每一列的选取状态用一个整数表示。
2.排列/组和问题,在状态比较少的时候也可以使用状压dp。
状压dp的好处就是在使用回溯法,dfs,bfs求解时,遍历所有的状态需要的时间复杂度是O(n!)级别的,但是状压dp可以将复杂度降为O(m^n),m为表示状态的使用的进制。
4.实现方式
1.自底向上
自底向上根据情况又有2种实现方式,【条件1】:自底向上要求在遍历到状态S时,所有能转移到它的状态的dp值已经计算完成。
(1).状态转移方程明确,对于一个状态S所有可以转移到S状态的状态集合c明确,并且容易获取。
就可以使用传统dp的实现方式,循环遍历每一个状态S,根据状态S的所有能转移到它的状态集合C,计算出该状态S的dp值。
这样就可以计算出所有状态的dp值了。
(2).不符合第(1)种情况。
如果不好找到上述状态S的集合c,或者找到该集合c需要的代价很大。
则可以遍历每一个有效状态S,然后根据S计算出所有它能转移到的状态,更新这些状态的dp值。
第(2)种的实现中,假如共有state种状态,在遍历到一个状态S时,要保证能转移到S的状态的dp值已经计算完成。
则
a). 可以通过for(int i = 0;i<state;i++)来遍历每一个有效状态,然后更新所有它可以转移到的状态的dp值;适用于状态只会由小转大(大部分情况都是这样),这种情况,此方法就可以满足条件1.
b). 可以通过队列来实现,类似于bfs;但是也得注意是否满足条件1。
2.自顶向下带备忘录DFS。