LeetCode #1349. 参加考试的最大学生数 - 学到了:压缩状态动态规划、位运算、reduce()、str().count()

赛题见: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会生成80b10000,其中最高位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 # 不存在

在为运算中,10 的性质还是蛮不同的。

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的数量正合适。

原创文章 163 获赞 177 访问量 4万+

猜你喜欢

转载自blog.csdn.net/weixin_42815609/article/details/104256847