一、题目
二、分析
1. 什么是动态规划(dynamic programming,DP)
很多情况下我们都把动态规划想得太复杂了,我也是其中一个。quora 上有一个回答是这么说的:
首先在一张纸上写下 1+1+1+1+1+1+1+1=?1+1+1+1+1+1+1+1=?1+1+1+1+1+1+1+1=?
“它等于多少呢?”
我们会立即脱口而出,“等于8!”
如果我们在左边添一个 1+1 +1+
“会等于多少呢?”
当然,不用想,“等于9!”
“为什么你会计算得这么快呢?”
“因为 8+1=98+1=98+1=9”
所以,我们没有重新计算 1+1+1+1+1+1+1+1+11+1+1+1+1+1+1+1+11+1+1+1+1+1+1+1+1 的值因为我们记住了前面的和等于 8,所以再次计算时只需再加 1 就可以了。
我们通过记住一些事情来节省时间,这就是动态规划的精髓。 具体来说,如果一个问题的子问题会被我们重复利用,我们则可以考虑使用动态规划。
2. 什么是状态压缩 DP
一般来说,动态规划使用一个一维数组或者二维数组来保存状态。
比如 42.接雨水 中,我们使用一维数组 dp[i] 表示下标 i左边最高柱子的高度。dp[i] 包含了两个信息:
下标 i 左边的柱子
最高的高度值
比如 10.正则表达式匹配 中,我们使用二维数组 dp[i][j] 表示 字符串 s 的前 i 项和 t 的前 j 项是否匹配。dp[i][j] 包含了三个信息:
s 的前 i 项
t 的前 j 项
是否匹配
对于本题来讲,通过分析,我们也可以表示类似的状态,dp[i][j] 表示当第 i 行的座位分布为 j 时,前 i 行可容纳的最大学生人数。但如果我们还想知道第 i 行有多少个座位呢?这无疑多了一个维度,这时我们不能用类似 dp[i][j][k] 来表示了,因为计算机中没有类似三维的数据结构。
这时候状态中所包含的信息过多,该怎么办呢?我们可以利用二进制以及位运算来实现对于本来应该很大的数组的操作,这就是状态压缩,而使用状态压缩来保存状态的 DP 就叫做状态压缩 DP。
3. 解题思路
1. 用位编码表示状态
题目中提到 m,n 不超过 8,图我们可以使用位编码记录每一行的状态,即当椅子上坐了一个人时,我们将该位置设为 1,如下图所示:
2. 判断此状态是否有效
对于如下图的座位中,我们看到坐了两个人,如何判断这个状态是不是有效的呢?首先将原状态左移,右移,如果它们的与运算结果为 0,则表明学生左侧和右侧没有人坐。此外,类似地,我们还要保证学生坐在良好的座位上。
3. 判断此状态 i 与下一个状态 i+1 的关系
与步骤 2 类似,将下一行的状态 i+1 分别左移,右移,再与状态 i 作与运算,来判断状态 i 的左上和右上是否有人,从而判断是否可以传递状态。
如果与运算结果为 0,表示左上或者右上没有人,满足传递状态,这时状态转移方程为 dp[i+1][j] = max(dp[i+1][j], dp[i][k] + count(j)),count(j) 表示 i+1 状态中 1 的数量。
三、代码
from functools import reduce
class Solution:
def maxStudents(self, seats: List[List[str]]) -> int:
m, n = len(seats), len(seats[0]),
dp = [[0]*(1 << n) for _ in range(m+1)] # 状态数组 dp
a = [reduce(lambda x,y:x|1<<y,[0]+[j for j in range(n) if seats[i][j]=='#']) for i in range(m)] # 将 # 设为 1,当遇到 . 时与运算结果为 0,表示可以坐人
# print(a)
for row in range(m)[::-1]: # 倒着遍历
for j in range(1 << n):
if not j & j<<1 and not j&j>>1 and not j & a[row]: # j & a[row]代表该位置可以坐人,j & j<<1 and not j&j>>1 表示该位置左右没人可以坐的
for k in range(1 << n):
if not j&k<<1 and not j&k>>1: # j状态的左上和右上没有人
dp[row][j] = max(dp[row][j], dp[row+1][k] + bin(j).count('1'))
# print(dp)
return max(dp[0])