在大小为 n * m
的棋盘中,有两种不同的棋子:黑色,红色。当两颗颜色不同的棋子同时满足以下两种情况时,将会产生魔法共鸣:
- 两颗异色棋子在同一行或者同一列
- 两颗异色棋子之间恰好只有一颗棋子
注:异色棋子之间可以有空位
由于棋盘上被施加了魔法禁制,棋盘上的部分格子变成问号。chessboard[i][j]
表示棋盘第 i
行 j
列的状态:
- 若为
.
,表示当前格子确定为空 - 若为
B
,表示当前格子确定为 黑棋 - 若为
R
,表示当前格子确定为 红棋 - 若为
?
,表示当前格子待定
现在,探险家小扣的任务是确定所有问号位置的状态(留空/放黑棋/放红棋),使最终的棋盘上,任意两颗棋子间都 无法 产生共鸣。请返回可以满足上述条件的放置方案数量。
示例1:
输入:
n = 3, m = 3, chessboard = ["..R","..B","?R?"]
输出:
5
解释:给定的棋盘如图:
{:height=150px} 所有符合题意的最终局面如图:
示例2:
输入:
n = 3, m = 3, chessboard = ["?R?","B?B","?R?"]
输出:
105
提示:
n == chessboard.length
m == chessboard[i].length
1 <= n*m <= 30
chessboard
中仅包含"."、"B"、"R"、"?"
解法
class Solution:
# 用于计算合法方案数
def getSchemeCount(self, n: int, m: int, chessboard: List[str]) -> int:
# 用于计算在当前行或列状态下,添加一个字符后的结果状态。
# pre: 当前状态, 是一个长度为2的字符串, 表示前两个字符。
# a: 要添加的字符,可以是'.', 'R', 'B'。
@cache
def get_next(pre, a):
# 如果要添加的字符是'.', 直接返回当前状态pre, 因为'.'不影响状态。
if a == '.':
return pre
# 处理'R'或'B': 如果当前状态的第一个字符是'.' 或者与要添加的字符相同( pre[0] in ['.', a]), 则返回新状态 pre[1] + a。
# 否则,返回空字符串'', 表示添加该字符后状态非法
# 添加 'R'或'B'的情况:
# 如果要添加的字符是'R'或'B',需要判断当前状态pre是否允许添加这个字符。
# 合法情况
# 如果当前状态的第一个字符是'.'或者与要添加的字符相同, 那么添加这个字符是合法的。
# 注意: 这里的 "第一个字符" 实际指的是索引为1的字符,也就是第二个。
# 第一个字符是'.': 表示当前状态是空的,或者不确定的, 可以添加任何字符。
# 第一个字符与要添加的字符相同:表示当前状态已经有一个相同的字符, 可以继续添加相同的字符。
# 举例:
# 如果 pre = '..', 添加 'R'后, 返回'.R' (因为第一个字符是'.', 可以添加'R')
# 如果 pre = '.R', 添加 'R'后, 返回'RR' (因为第一个字符是'R', 可以继续添加'R')
# 如果 pre = '..', 添加 'B'后, 返回'.B' (因为第一个字符是'.', 可以添加'B')
# 所有合法情况:
# pre = '..',a = '.' → 返回 pre = '..'
# pre = '.R',a = '.' → 返回 pre = '.R'
# pre = '.B',a = '.' → 返回 pre = '.B'
# pre = 'RR',a = '.' → 返回 pre = 'RR'
# pre = 'RB',a = '.' → 返回 pre = 'RB'
# pre = 'BR',a = '.' → 返回 pre = 'BR'
# pre = 'BB',a = '.' → 返回 pre = 'BB'
# pre = '..',a = 'R' → 返回 .R(因为第一个字符是 '.',可以添加 'R')
# pre = '.R',a = 'R' → 返回 RR(因为第一个字符是 'R',可以继续添加 'R')
# pre = 'RR',a = 'R' → 返回 RR(因为第一个字符是 'R',可以继续添加 'R')
# pre = '..',a = 'B' → 返回 .B(因为第一个字符是 '.',可以添加 'B')
# pre = '.B',a = 'B' → 返回 BB(因为第一个字符是 'B',可以继续添加 'B')
# pre = 'BB',a = 'B' → 返回 BB(因为第一个字符是 'B',可以继续添加 'B')
return pre[1] + a if pre[0] in ['.', a] else ''
# 用于深度优先搜索所有可能的合法方案。它的参数包括:
# i: 当前行号
# j: 当前列号
# row: 当前行的状态
# cols: 一个元祖, 表示每一列的状态
@cache
def dfs(i, j, row, cols):
# 递归终止条件
# 如果i==n, 表示已经处理完所有行, 找到一个合法方案, 返回1.
if i == n:
return 1
ans = 0
# 遍历可能的字符
# 如果当前棋盘位置是'?', 尝试'.', 'R'和'B'。
# 如果当前棋盘位置是'.', 只尝试'.'。
# 如果当前棋盘位置是'R' 或 'B', 只尝试对应的字符。
for a in ('.RB' if chessboard[i][j] == '?' else chessboard[i][j]):
# 状态转移
# 对于每个可能的字符a, 调用get_next函数计算新的行状态nrow 和新的列状态ncol
# 如果nrow和ncol都不为空(即状态合法), 继续递归:
# 如果 j < m-1, 表示当前行还有更多列, 更新j和row。
# 如果 j == m-1, 表示当前行已经处理完, 更新i和row。
# 更新列状态cols。
# 计算新的行状态nrow:
# 调用get_next(row, a), 将结果赋值给变量nrow。
# 如果get_next(row, a), 返回空字符串'', 表示当前字符 a 无法添加到当前行状态row中(即行状态非法), nrow 为'', 整个条件为False, 不会进入if语句块
# 计算新的列状态ncol:
# 如果nrow不为空, 继续调用 get_next(cols[j], a), 将结果赋值给变量ncol。
# 如果get_next(cols[j], a)返回空字符串'', 表示当前字符a无法添加到当前列状态 cols[j]中(即列状态非法), ncol为'', 整个条件为False, 不会进入if语句块
if (nrow := get_next(row, a)) and (ncol := getnext(cols[j], a)):
# 更新了递归的索引和状态,准备进入下一个位置
# 如果当前列j不是最后一列:
# ni = i: 行号保持不变
# nj = j+1: 列号加1, 移动到下一列
# nrow = nrow: 行状态保持为当前计算的nrow。
# 如果当前列j是最后一列:
# ni = i+1: 行号加1, 移动到下一行
# nj = 0: 列号重置为0, 从下一行的第一列开始
# nrow = '..' 行状态重置为空状态, 因为开始新的一行
ni, nj, nrow = (i, j + 1, nrow) if j < m - 1 else (i + 1, 0, '..')
# 累加结果: 递归调用dfs函数, 继续探索下一个为止的可能方案,将所有合法路径的结果累加到ans。
# cols[:j]+(ncol,)+cols[j+1:] : 更新列的状态cols, 将新的列状态ncol放入第j列, 其余列保持不变
ans += dfs(ni, nj, nrow, cols[:j] + (ncol,) + cols[j+1:])
return ans
return dfs(0, 0, '..', ('..',)*m)
# 状态转换:
分类讨论,对于一行或者一列的 RB 序列,有以下 7 种情况:
# 空。
# 只有一个 B。
# 只有一个 R。
# 连续多个 B。
# 连续多个 R。
# BR 交替,且以 B 结尾。
# BR 交替,且以 R 结尾。
# 为什么单独一个 B/R 也算一个状态?因为既可以变成连续多个 B/R,又可以变成 BR 交替。而「连续多个 B/R」和 「BR 交替」是无法互相转换的。
# 遍历棋盘的过程就是在不断生成序列的过程,那么向序列末尾添加 B 或者 R,这些状态就会互相转换,形成一个 7⋅2 的转换关系表,用数组 trans 记录,其中 −1 表示非法转换。