【Leetcode】2056. 棋盘上有效移动组合的数目——2611
题目
2056. 棋盘上有效移动组合的数目
有一个 8 x 8 的棋盘,它包含 n 个棋子(棋子包括车,后和象三种)。给你一个长度为 n 的字符串数组 pieces ,其中 pieces[i] 表示第 i 个棋子的类型(车,后或象)。除此以外,还给你一个长度为 n 的二维整数数组 positions ,其中 positions[i] = [ri, ci] 表示第 i 个棋子现在在棋盘上的位置为 (ri, ci) ,棋盘下标从 1 开始。
棋盘上每个棋子都可以移动 至多一次 。每个棋子的移动中,首先选择移动的 方向 ,然后选择 移动的步数 ,同时你要确保移动过程中棋子不能移到棋盘以外的地方。棋子需按照以下规则移动:
- 车可以 水平或者竖直 从
(r, c)沿着方向(r+1, c),(r-1, c),(r, c+1)或者(r, c-1)移动。 - 后可以 水平竖直或者斜对角 从
(r, c)沿着方向(r+1, c),(r-1, c),(r, c+1),(r, c-1),(r+1, c+1),(r+1, c-1),(r-1, c+1),(r-1, c-1)移动。 - 象可以 斜对角 从
(r, c)沿着方向(r+1, c+1),(r+1, c-1),(r-1, c+1),(r-1, c-1)移动。
移动组合 包含所有棋子的 移动 。每一秒,每个棋子都沿着它们选择的方向往前移动 一步 ,直到它们到达目标位置。所有棋子从时刻 0 开始移动。如果在某个时刻,两个或者更多棋子占据了同一个格子,那么这个移动组合 不有效 。
请你返回 有效 移动组合的数目。
注意:
- 初始时,不会有两个棋子 在 同一个位置 。
- 有可能在一个移动组合中,有棋子不移动。
- 如果两个棋子 直接相邻 且两个棋子下一秒要互相占据对方的位置,可以将它们在同一秒内 交换位置 。
提示:
n == pieces.lengthn == positions.length1 <= n <= 4pieces只包含字符串"rook","queen"和"bishop"。- 棋盘上最多只有一个后。
1 <= ri, ci <= 8- 每一个
positions[i]互不相同。# 2056. 棋盘上有效移动组合的数目
思路
首先,题目要求我们计算棋盘上所有可能的“有效”移动组合。棋盘上有多种棋子(车、后、象),每个棋子可以在指定的规则下移动,每个棋子最多移动一次。我们需要确保在任何时刻,棋盘上的两个棋子不能占据相同的位置。
核心问题在于:
- 每个棋子的移动规则不同:车只能水平和竖直移动,后可以水平、竖直和对角线移动,而象只能对角线移动。
- 确保棋子之间不会发生冲突:即同一时刻不能有两个棋子占据相同的格子。
为了计算有效的移动组合,我们需要遍历所有棋子在所有可能位置的组合。由于题目中棋子的数量最多为 4,因此,暴力枚举所有可能的棋子移动组合是可以接受的。
关键点:
- 每个棋子的移动是独立的:每个棋子有自己独立的可选移动位置。
- 回溯策略:我们需要通过递归(深度优先搜索)遍历每个棋子的所有可选位置,并在遇到无效组合时回溯,尝试其他可能的路径。
- 冲突检测:每次移动后,我们需要检查当前状态是否有效,确保没有两个棋子占据同一个位置。
选择棋子不移动:处理当前棋子不动的情况
在处理每个棋子时,我们首先要判断是否可以让当前棋子停在原位置不动。为了防止两个棋子重叠在同一个位置,我们需要确保当前棋子的原位置没有被前面的棋子占据。
if all(pos[i][0] * 9 + pos[i][1] not in had[j] for j in range(i)):
last.append(pos[i][0] * 9 + pos[i][1]) # 标记当前棋子的位置
res += dfs(i + 1) # 继续递归下一个棋子
last.pop() # 回溯,撤销这个操作
这里使用了 all 判断当前棋子的位置是否没有被其他棋子占据。如果没有被占据,我们将当前位置添加到 last 中,然后递归处理下一个棋子,最后再回溯,撤销这次选择。
选择棋子移动:处理当前棋子选择移动的情况
如果棋子选择移动,我们需要遍历它的所有可能移动方向。对于每个方向,我们逐步检查每一步的可行性,直到该棋子无法继续移动为止。每次尝试移动一个格子时,我们需要进行冲突检查,确保当前移动不会导致棋子碰撞。
for dr, dc in dxy[pieces[i]]:
t = []
r, c = pos[i]
step = 0 # 走的步数
while 8 >= r + dr >= 1 and 8 >= c + dc >= 1: # 判断是否越界
r += dr
c += dc
step += 1 # 增加步数
确保当前移动没有冲突
在每一步移动时,我们需要确保当前的格子没有被其他棋子占据,或者没有其他棋子阻挡着路径。我们需要检查以下几种情况:
- 该位置在当前棋子移动时已经被其他棋子占据;
- 当前棋子走到的位置时其他棋子也恰好走到这里,无法继续移动。
f = 0
for j in range(i):
if r * 9 + c == last[j] and had[j][r * 9 + c] <= step:
f = 1
break
if r * 9 + c in had[j] and had[j][r * 9 + c] == step:
f = 1
break
在每次递归结束后,我们需要撤销当前棋子的移动,以便尝试其他的可能路径。
复杂度与性能
由于每个棋子最多有 15 种可选的移动方式(对于车而言),并且最多 4 个棋子,我们的方案的时间复杂度大约为 \(O(15^n)\),其中 n 为棋子的数量。由于$ n <= 4$,这个复杂度在题目给定的约束下是可以接受的。
代码
dxy = {
"queen": [(1, 1), (1, -1), (-1, 1), (-1, -1), (0, 1), (0, -1), (1, 0), (-1, 0)],
"bishop": [(1, 1), (1, -1), (-1, 1), (-1, -1)],
"rook": [(0, 1), (0, -1), (1, 0), (-1, 0)]
}
class Solution:
def countCombinations(self, pieces: List[str], pos: List[List[int]]) -> int:
n = len(pieces) # 棋子的数量
had = [dict() for _ in range(n)]
# 设置初始棋子的位置
# k 表示当前棋子到过这个位置
# v 表示何时到达这个位置的
for i, (r, c) in enumerate(pos):
had[i][r * 9 + c] = 0
last = [] # 棋子最终的位置
def dfs(i):
if i >= n: return 1
res = 0
# 当前棋子能否选择不移动?
if all(pos[i][0] * 9 + pos[i][1] not in had[j] for j in range(i)):
last.append(pos[i][0] * 9 + pos[i][1])
res += dfs(i + 1)
last.pop()
# 当前棋子选择移动
for dr, dc in dxy[pieces[i]]:
t = list()
r, c = pos[i]
step = 0 # 行走的步数
while 8 >= r + dr >= 1 and 8 >= c + dc >= 1:
r += dr
c += dc
step += 1 # 走了一步
# 如果当前位置当前时刻已经有棋子了
f = 0
for j in range(i):
# 1. 判断之前棋子放到这个位置了(前有停车,过不去)
if r*9+c == last[j] and had[j][r*9+c] <= step:
f = 1
break
# 2. 之前棋子在行进的过程中放到这个位置了(行进过程碰车)
if r*9+c in had[j] and had[j][r*9+c] == step:
f = 1
break
if f == 1: break
# 3. 若打算放到这个位置,还需要判断
# 如果停在这个位置,是否之前的就到达不了了
# (若停车,是否阻碍其他人行驶?)
# 如果当前位置当前时刻没有棋子
had[i][r*9+c] = step
t.append(r*9+c)
if any(r*9+c in had[j] and had[j][r*9+c] >= step for j in range(i)):
continue
# 当前棋子放到了(r, c)位置
last.append(r*9+c)
res += dfs(i + 1)
last.pop()
# 回溯
for x in t:
del had[i][x]
return res
return dfs(0)

浙公网安备 33010602011771号