LCP 76. 魔法棋盘

在大小为 n * m 的棋盘中,有两种不同的棋子:黑色,红色。当两颗颜色不同的棋子同时满足以下两种情况时,将会产生魔法共鸣:

  • 两颗异色棋子在同一行或者同一列
  • 两颗异色棋子之间恰好只有一颗棋子

    注:异色棋子之间可以有空位

由于棋盘上被施加了魔法禁制,棋盘上的部分格子变成问号。chessboard[i][j] 表示棋盘第 i 行 j 列的状态:

  • 若为 . ,表示当前格子确定为空
  • 若为 B ,表示当前格子确定为 黑棋
  • 若为 R ,表示当前格子确定为 红棋
  • 若为 ? ,表示当前格子待定

现在,探险家小扣的任务是确定所有问号位置的状态(留空/放黑棋/放红棋),使最终的棋盘上,任意两颗棋子间都 无法 产生共鸣。请返回可以满足上述条件的放置方案数量。

示例1:

输入:n = 3, m = 3, chessboard = ["..R","..B","?R?"]

输出:5

解释:给定的棋盘如图:

image.png

{:height=150px} 所有符合题意的最终局面如图:

image.png

示例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 表示非法转换。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值