状态压缩动态规划
状态压缩动态规划就是我们常说的状压DP,前两天某厂实习生二面面试官随手就给我抽了一道状压DP的题,我根本没思路,sorry就写了一行注释。然后leetcode周赛最后一题又碰到了状压DP的题目,我一定要搞定这个类型的问题。加油加油!
状压DP说简单也简单,基本上就是用一串二进制树来表示当前情况 的状态,例如举个在别人的博客里看到的例子:
题目:在 n*n(n≤20)的方格棋盘上放置 n 个车(可以攻击所在行、列),求使它们不能互相攻击的方案总数。
通过数学方法很好理解,就是n的阶乘,n!。
那如果用动态规划来解决呢?
我们可以把问题抽象一下,用一串二进制数来表示当前状态每一列上放置车的情况,比如在 n = 5 n=5 n=5的情况下, 00010 00010 00010就代表第2列上已经放置了一辆车(从右往左数),注意一下,这个状态表明的只是第二列上有一辆车,并没有限制这一辆车在第几行,其实放在第几行对最后的结果并没有什么影响,可以看作是按行放置的顺序可以不同,结果数是一样的(我是这么理解的)。我们用一个数组 d p [ s t a t u s ] dp[status] dp[status]来表示 s t a t u s status status这个状态下的可能方案数。初始条件的话, d p [ 00000 ] dp[00000] dp[00000]很明显就只有一种情况,所以 d p [ 00000 ] = 1 dp[00000]=1 dp[00000]=1。那么问题要求的结果很明显就是每一列都有一种车的情况,也就是 d p [ 11111 ] dp[11111] dp[11111]。那么我们只要找到递推公式,就可以一点一点的把所有状态都求出来。有一点值得注意的是,我们用来代表状态的五位二进制数是可以转换为十进制的,也就是说可以把它们看作是dp数组的下标,例如00000就是0, 00011就是3这样。也可以根据状态查询dp数组找到要查询的状态的值。
那么我们来看一看当所有列之中一共只有一列有车的情况下,它在dp数组中的值为多少。比如状态00010, 它是在00000状态之上,在第二列上放了一个车,所以它的方案数就是dp[00000]这么多的方案数。所以
d
p
[
00010
]
=
d
p
[
00000
]
=
1
dp[00010]=dp[00000]=1
dp[00010]=dp[00000]=1。同样对于其他几个只有一个列有车的情况有
d
p
[
10000
]
=
d
p
[
01000
]
=
d
p
[
00100
]
=
d
p
[
00001
]
=
1
dp[10000]=dp[01000]=dp[00100]=dp[00001]=1
dp[10000]=dp[01000]=dp[00100]=dp[00001]=1。
那么我们再看所有列中有两个列有车的状态,他们的状态下有多少种方案,例如状态11000下,它的前一个状态可能是10000也可能是01000,所以它的dp值为这两个状态的和,也就是
d
p
[
11000
]
=
d
p
[
10000
]
+
d
p
[
01000
]
=
2
dp[11000]=dp[10000]+dp[01000]=2
dp[11000]=dp[10000]+dp[01000]=2,而五列里有两个车的状态有
C
5
2
=
10
C^2_5=10
C52=10个,这十个状态的dp值也就是对应的方案书都是2。
在看三个列有车的状态,例如11100,它的上一个状态可能是11000或者10100或者01100这三种,所以它的可能性数就是对应的三种状态的值的和。
d
p
[
11100
]
=
d
p
[
11000
]
+
d
p
[
10100
]
+
d
p
[
01100
]
dp[11100]=dp[11000]+dp[10100]+dp[01100]
dp[11100]=dp[11000]+dp[10100]+dp[01100]。
所以我们可以得出递推公式:
d
p
[
s
t
a
t
u
s
]
=
∑
i
=
0
i
<
n
a
n
d
(
s
t
a
t
u
s
&
(
1
<
<
i
)
)
d
p
[
s
t
a
t
u
s
^
(
1
<
<
i
)
]
dp[status]=\sum_{i=0}^{i<n\ and\ (status\ \&\ (1<<i)) }dp[status\text{\textasciicircum}{(1<<i)}]
dp[status]=i=0∑i<n and (status & (1<<i))dp[status^(1<<i)]
这个式子乍一看可能很懵,我们一点一点看。先看求和符号上面的部分,
i
<
n
i<n
i<n很好理解,就是每个状态有n个位,所以
i
∈
[
0
,
n
)
i\in[0,n)
i∈[0,n),一共出现n次。而右半部分
s
t
a
t
u
s
&
(
1
<
<
i
)
status\ \&\ (1<<i)
status & (1<<i)我们可以这么理解,对于一个
s
t
a
t
u
s
status
status,我们可以通过用一个只有一位为1的数和它进行与操作,来判断这一位是不是1,因为如果这一位是1的话,结果必然不为0,如果不为1,结果便为0。
举个例子,比如状态11100,它和00100相与结果为00100,不为0,所以第三位上不为0,其中
1
<
<
i
1<<i
1<<i便是控制第i+1位为1的表达式,而这个时候就可以进行右边的操作
s
t
a
t
u
s
^
(
1
<
<
i
)
status\text{\textasciicircum}{(1<<i)}
status^(1<<i),这一部分便是找到放置第i+1位小车之前的状态,举个例子,比如说状态11100, 它和00100异或的结果是11000,也就是在放置第三位小车之前的状态,所以把所有位都进行一次这样的操作,再把他们的状态值求和,便是新的状态值了。
我认为状态压缩动态规划就是利用二进制的性质来简化动态规划的一些操作的一种方法,感觉就像通过二进制性质把当前状态哈希到一个整数,然后在通过哈希之前的状态来获取值再进行递推的一种方法,以上就是我的理解,希望能给大家带来帮助!(估计也没人会看 23333)。