文章目录
引入
在本周的周赛中,遇到了这么一道题🔗:
1349.参加考试的最大学生数
给你一个 m * n 的矩阵 seats 表示教室中的座位分布。如果座位是坏的(不可用),就用 ‘#’ 表示;否则,用 ‘.’ 表示。
学生可以看到左侧、右侧、左上、右上这四个方向上紧邻他的学生的答卷,但是看不到直接坐在他前面或者后面的学生的答卷。请你计算并返回该考场可以容纳的一起参加考试且无法作弊的最大学生人数。
学生必须坐在状况良好的座位上。
…
看到这道题的时候,直接就想到了N皇后问题,采用回溯法+剪枝即可。
所以我的代码是这样的:
class Solution {
char[][] seats;
int count;
public int maxStudents(char[][] seats) {
this.seats = seats;
count = 0;
int row = seats.length;
int col = seats[0].length;
helper(row * col - 1, 0, row, col);
return count;
}
public void helper(int index, int curr, int row, int col) {
if (index < 0) {
count = Math.max(curr, count);
return;
}
int posRow = index / col;
int posCol = index % col;
// System.out.println(index+" "+(index / col)+" "+(index % col) );
if (check(posRow, posCol, row, col) && seats[posRow][posCol] == '.') {
seats[posRow][posCol] = '!';
helper(index - 1, curr + 1, row, col);
seats[posRow][posCol] = '.';
}
helper(index - 1, curr, row, col);
}
public boolean check(int posRow, int posCol, int row, int col) {
// System.out.println(posRow+" "+posCol);
if (posRow + 1 < row && posCol + 1 < col && seats[posRow + 1][posCol + 1] == '!') {
return false;
} else if (posRow + 1 < row && posCol - 1 >= 0 && seats[posRow + 1][posCol - 1] == '!') {
return false;
} else if (posCol + 1 < col && seats[posRow][posCol + 1] == '!') {
return false;
} else if (posCol - 1 >= 0 && seats[posRow][posCol - 1] == '!') {
return false;
}
return true;
}
}
从最后一个位置开始坐考生,然后逐一往前推进。
然而这样做,使得时间复杂度太高了,很明显是O(N!): 放置第 1 个考生有 N 种可能的方法,放置两个考生的方法不超过 N (N - 2) ,放置 3 个考生的方法不超过 N(N - 2)(N - 4)。
压缩状态动态规划解法
一般来说,动态规划使用一个一维数组或者二维数组来保存状态。
比如 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。
解题思路
- 用位编码表示状态
题目中提到 m,n 不超过 8,图我们可以使用位编码记录每一行的状态,即当椅子上坐了一个人时,我们将该位置设为 1,如下图所示:
- 判断此状态是否有效
对于如下图的座位中,我们看到坐了两个人,如何判断这个状态是不是有效的呢?首先将原状态左移,右移,如果它们的与运算结果为 0,则表明学生左侧和右侧没有人坐。此外,类似地,我们还要保证学生坐在良好的座位上。
- 判断此状态 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
# print(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]: # 倒着遍历
print(row)
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])