赛题见:https://leetcode-cn.com/problems/maximum-students-taking-exam/
我的解法是用递归实现广度优先搜索,结果是对的,但是太慢,超时了。这种问题原来可以用压缩状态动态规划来解:
- 同学在第
i
排(第i
个单位的情况)能否入座,除了与第i
排有关,还与第i+1
排有关,因此可以用所谓的“动态规划”,从最后一个单位开始遍历就好,遍历到第1
个单位,得到的方案肯定是最优方案; - 方案可以用一个二元的序列
[0, 1, 1, 1, 0, 1, ...]
来表示,因此没有必要给每个元素一个空间int
,将整个序列压缩为一个二进制数,并在运算时使用位运算可大大提升时间空间效率。
这里的动态规划与运筹学中的动态规划思想一致。 就是解决了多阶段决策问题,上一阶段的决策对本阶段决策有影响(本题中,后一排的座位排布对本排决策有影响)。动态规划中,要建立二维数组进行状态的记录,
d[i][j]=q
中,i
代表第 i 阶段决策,j
代表第 j 个方案,值q
代表该决策带来的总收益(包括该决策之前的决策的影响)。本题中,方案j
可以用状态压缩来表示,属于妙用位运算。
先来看题解:
作者:ml-zimingmeng
链接:https://leetcode-cn.com/problems/maximum-students-taking-exam/solution/xiang-jie-ya-suo-zhuang-tai-dong-tai-gui-hua-jie-f/
来源:力扣(LeetCode)
from functools import reduce
m, n = len(seats), len(seats[0]),
dp = [[0]*(1 << n) for _ in range(m+1)] # 状态数组 dp
"""
m+1 是因为,下面的动态规划是从最后一行开始的,
dp[i][j]记录第 i 行方案 j 对应的第最后一行到现在第 i 行已经坐下的同学数量
因此 d[0] 中存储的最大值就是最优方案,而 d[-1] 就是全为 0 ,为真实的最后一行 d[-2] 做铺垫的
"""
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]: # 倒着遍历
print(row)
for j in range(1 << n):
if not j & j<<1 and not j&j>>1 and not j & a[row]:
# not j & a[row]代表该位置可以坐人,not 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])
我加了一点注释。其中,涉及到一些二进制运算的技巧,我花了些时间将其总结在下面。还有两个 python3
的技巧,我将其放在文章最后一个部分。
位运算与常用技巧
1. 位运算中,1<<n
,生成 1 + 0 of n
1<<n
其实就是将1左移n位的意思,1<<4
会生成8
即0b10000
,其中最高位1
往往是不参与运算的,只是为了表示一些目前的运算单元是几位的而已。
2. 位运算中,如下方法检测 xx11xx
是否存在
观察如下程序。
>>> for j in range(1<<3):
... print(bin(j))
... print(bin(j<<1))
... print(j&j<<1)
...
0b0
0b0
0
0b1
0b10
0
0b10
0b100
0
0b11
0b110
2
0b100
0b1000
0
0b101
0b1010
0
0b110
0b1100
4
0b111
0b1110
6
可见,&
是按位的与运算。只要有一个位同为 1
,那么 &
就会返回 True
。
因此,可以用如下程序检查bin_arr
是否有连续的 11
出现。
if not bin_arr & bin_arr<<1 and bin & bin_arr>>1:
return True # 存在
return False # 不存在
在为运算中,1
和 0
的性质还是蛮不同的。
reduce() 和 str().count()
reduce() 累加
reduce()
让代码更加优雅了。
为了把[["#",".","."],[".","#","."]]
提取成[0b100][0b010]
,其中0
代表作为可用,1
反之;作者只用了一行:
a = [reduce(lambda x,y:x|1<<y,[0]+[j for j in range(n) if seats[i][j]=='#']) for i in range(m)]
我来把其拆开:
def foo(x, number):
return x | 1<<number
a = []
for i in range(m):
iter_tmp = 0
for j in range(n):
if seat[i][j] == '#':
iter_tmp = foo(iter_tmp, j)
a.append(iter_tmp)
可见,如果理解 reduce()
、lambda
与列表生成机制,作者的一行代码不但简洁,还易读。
str().count()
bin(int)
将int
转换成0b10101101
这个形式的字符串,因此用str().count('1')
的形式来返回1
的数量正合适。