原帖
http://blog.csdn.net/lmyclever/article/details/6671923
我们知道,用DP解决一个问题的时候很重要的一环就是状态的表示,一般来说,一个数组即可保存状态。但是有这样的一些题目,它们具有DP问题的特性,但是状态中所包含的信息过多,如果要用数组来保存状态的话需要四维以上的数组。于是,我们就需要通过状态压缩来保存状态,而使用状态压缩来保存状态的DP就叫做状态压缩DP。
一道例题:
HOJ 2662
有一个n*m的棋盘(n、m≤80,n*m≤80)要在棋盘上放k(k≤20)个棋子,使得任意两个棋子不相邻(每个棋子最多和周围4个棋子相邻)。求合法的方案总数。
直接考虑解决这个问题并不容易,我们先来考虑这个问题的退化形式:
现在我们令n=1。则我们可以很容易的想到状态转移方程:
设dp[i][j][0]表示当前到达第i列,一共使用了j个旗子,且当前格子的状态为不放的状态总数,类似的 dp[i][j][1]就是当前格子的状态为放的状态总数。
那么状态转移方程就是
dp[i][j][0]=dp[i-1][j][1]+dp[i-1][j][0];
dp[i][j][1]=dp[i-1][j-1][0];
当n=1的时候这个问题无疑是非常简单的,但是如果我们想模仿这种做法来解决原问题的话,就会遇到这样的问题:如何来表示当前行的状态?
昨天已经提到了一些状态压缩的知识,如果看懂了的话,应该已经明白怎么做了。
对于每一行,如果把没有棋子的地方记为0,有棋子的地方记为1,那么每一行的状态都可以表示成一个2进制数,进而将其转化成10进制。
那么这个问题的状态转移方程就变成了
设dp[ i ] [ j ][k ]表示当前到达第i列,一共使用了j个棋子,且当前行的状态在压缩之后的十进制数为k 时的状态总数。那么我们也可以类似的写出状态转移方程:
dp[ i ][ j ][ k ]=sum( dp[ i-1][ j-num(k) ][ w ] ) num(k)表示 k状态中棋子的个数,w表示前一行的状态。
虽然写出了状态转移方程,但是还是有很多细节问题需要解决:比如,如何保证当前状态是合法的?
最基本的做法是:首先判断k状态是否合法,也就是判断在这一行中是否有2个旗子相邻,然后枚举上一行的状态w,判断w状态是否合法,然后判断k状态和w状态上下之间是否有相邻的棋子。
当然这样做的时间复杂度是很高的,也就是说有很多地方可以优化,比如:判断每一行状态是否合法,可以在程序一开始判断然后保存结果,判断k状态和w状态上下之间是否有相邻的棋子,可以利用位运算,if(k&w)说明上下之间有相邻的棋子等等。
讲到这里,这道题目的做法已经很明确了,请大家自行完成。
另一道例题:
TSP问题
给你n个城市和城市之间的通路的长度,请你找出一条经过所有城市一次且仅经过一次的路线,使得这条路线的长度最短。
问题分析,如果要设计一个状态的话,显然状态与已经走过的城市和你当前所在的城市有关,现在,按照一定的顺序给每个城市一个编号,如果已经走过的城市记为1,没走过的城市记为0,那么已经走过的城市的状态就可以压缩成一个数。所以,该题目的状态表示为:
Dp[i][j]表示已经走过的城市为i,当前所在的城市为j的最短路程。
相应的状态转移方程为dp[ i ][ j]=min( dp[ i ^ (1<<j) ][ k ] + dis[ k ][ j ] ); i ^ (1<<j)的意思是将j这个城市从i状态中去掉。 dis[ k][ j ] 是k和j之间的距离。
HOJ2665 虽然题意与之相去甚远,但是本质上是一样的,希望大家能够完成这道题目。
应用:
编程之美 瓷砖铺地扩展问题, 1*2瓷砖如何铺N*M.
状态压缩DP的特点:
状态中的某一维会比较小,一般不会超过15,多了的话状态数会急剧上升而无法压缩,一般来说需要状态压缩的也就是这一维。
状态压缩DP的常见优化:
预处理是最常见的优化,尤其是在棋盘类问题上,比如说例题1,如果我们想进一步提高效率,我们还可以预处理出状态之间是否可以转移而不用在每一次转移中判断。
灵活运用位运算,例题1中if(k&w)就是一个很好的例子。
推荐题目:
除了上述2道题目以外,我还推荐:
HOJ
• 2188 WordStack
• 2798 Globulous gumdrops
• 2800 Artillery Assignment
这一类DP对于编码能力要求比较高,请大家尽力而为。