Problem

A game is played by a cat and a mouse named Cat and Mouse.

The environment is represented by a grid of size rows x cols, where each element is a wall, floor, player (Cat, Mouse), or food.

  • Players are represented by the characters 'C'(Cat),'M'(Mouse).
  • Floors are represented by the character '.' and can be walked on.
  • Walls are represented by the character '#' and cannot be walked on.
  • Food is represented by the character 'F' and can be walked on.
  • There is only one of each character 'C', 'M', and 'F' in grid.

Mouse and Cat play according to the following rules:

  • Mouse moves first, then they take turns to move.
  • During each turn, Cat and Mouse can jump in one of the four directions (left, right, up, down). They cannot jump over the wall nor outside of the grid.
  • catJump, mouseJump are the maximum lengths Cat and Mouse can jump at a time, respectively. Cat and Mouse can jump less than the maximum length.
  • Staying in the same position is allowed.
  • Mouse can jump over Cat.

The game can end in 4 ways:

  • If Cat occupies the same position as Mouse, Cat wins.
  • If Cat reaches the food first, Cat wins.
  • If Mouse reaches the food first, Mouse wins.
  • If Mouse cannot get to the food within 1000 turns, Cat wins.

Given a rows x cols matrix grid and two integers catJump and mouseJump, return true if Mouse can win the game if both Cat and Mouse play optimally, otherwise return false.

https://leetcode.cn/problems/cat-and-mouse-ii/

Example 1:

Input: grid = ["####F","#C...","M...."], catJump = 1, mouseJump = 2
Output: true
Explanation: Cat cannot catch Mouse on its turn nor can it get the food before Mouse.

Example 2:

Input: grid = ["M.C...F"], catJump = 1, mouseJump = 4
Output: true

Example 3:

Input: grid = ["M.C...F"], catJump = 1, mouseJump = 3
Output: false

Constraints:

  • rows == grid.length
  • cols = grid[i].length
  • 1 <= rows, cols <= 8
  • grid[i][j] consist only of characters 'C', 'M', 'F', '.', and '#'.
  • There is only one of each character 'C', 'M', and 'F' in grid.
  • 1 <= catJump, mouseJump <= 8

Test Cases

1
2
class Solution:
def canMouseWin(self, grid: List[str], catJump: int, mouseJump: int) -> bool:
solution_test.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pytest

from solution import Solution


@pytest.mark.parametrize('grid, catJump, mouseJump, expected', [
(["####F","#C...","M...."], 1, 2, True),
(["M.C...F"], 1, 4, True),
(["M.C...F"], 1, 3, False),

(["..#C","...#","..M.","#F..","...."], 2, 1, True),
])
@pytest.mark.parametrize('sol', [Solution()])
def test_solution(sol, grid, catJump, mouseJump, expected):
assert sol.canMouseWin(grid, catJump, mouseJump) == expected

Thoughts

913. Cat and Mouse 的进阶版,除了地图和输赢的规则更复杂,还增加了老鼠的步数限制。

整体处理方案完全一致,即先找出所有确定输赢的状态,然后从每个状态倒推其上游状态,直到待处理的状态队列清空。最终看初始状态是否得到了确定的结果。

直接套用 913. Cat and Mouse 的代码,根据本题的具体规则做微调即可。

同样用三元组表示游戏的任何一个状态:state = (mouse, cat, moving) 分别是老鼠所在位置、猫所在位置、当前该谁移动。本题中的位置也是二元组如 (r, c) 表示玩家所在的行和列。

设食物所在的位置为 f0,那么对于任意(不是墙或食物的)位置 (r, c)(f0, (r, c), CAT) 表示老鼠吃到食物即老鼠赢,((r, c), f0, MOUSE) 表示猫吃到食物即猫赢,((r, c), (r, c), *) 表示猫抓到老鼠即猫赢。

设初始时老鼠和猫的位置分别为 m0 和 c0,那么初始状态为 initial = (m0, c0, MOUSE)

prev_states 方法用来枚举指定状态的所有上游状态,为了减少计算量,本题没再用 generator,而是直接返回上游状态数组并加了缓存避免重复计算。move_cnt 方法返回指定状态的所有下游状态的总数。注意本题允许玩家停在原地不动。

另外题目条件中限制如果老鼠在 1000 步内吃不到食物就算猫赢,不过按照给定的棋盘规模,这种情况很难发生,测试用例中也没有,因此并未实现。如果需要实现,可以在记录每个状态的(最终)输赢结果的同时,记录该状态到实际结果状态的最小步数。

Code

solution.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
from collections import deque
from functools import cache

Player = int
Position = tuple[int, int]
State = tuple[Position, Position, Player] # (mouse position, cat position, current moving player)


class Solution:
def canMouseWin(self, grid: list[str], catJump: int, mouseJump: int) -> bool:
_, MOUSE, CAT = range(3)
dirs = [(-1, 0), (1, 0), (0, -1), (0, 1)]
m = len(grid)
n = len(grid[0])
winners: dict[State, Player] = {} # state -> winner
queue: list[State] = deque()

# Find the initial state.
m0 = c0 = f0 = (0, 0)
for r, row in enumerate(grid):
for c, cell in enumerate(row):
if cell == 'M':
m0 = (r, c)
elif cell == 'C':
c0 = (r, c)
elif cell == 'F':
f0 = (r, c)

@cache
def prev_states(mouse: Position, cat: Position, moving: Player) -> list[State]:
states = []
if moving == MOUSE:
states.append((mouse, cat, CAT))
for dr, dc in dirs:
r, c = cat
for _ in range(catJump):
r -= dr
c -= dc
if r < 0 or r >= m or c < 0 or c >= n or grid[r][c] == '#':
break
states.append((mouse, (r, c), CAT))
else:
states.append((mouse, cat, MOUSE))
for dr, dc in dirs:
r, c = mouse
for _ in range(mouseJump):
r -= dr
c -= dc
if r < 0 or r >= m or c < 0 or c >= n or grid[r][c] == '#':
break
states.append(((r, c), cat, MOUSE))

return states

def move_cnt(mouse: Position, cat: Position, moving: Player) -> int:
cnt = 1 # 1 for staying at the same position
if moving == MOUSE:
for dr, dc in dirs:
r, c = mouse
for _ in range(mouseJump):
r += dr
c += dc
if r < 0 or r >= m or c < 0 or c >= n or grid[r][c] == '#':
break
cnt += 1
else:
for dr, dc in dirs:
r, c = cat
for _ in range(catJump):
r += dr
c += dc
if r < 0 or r >= m or c < 0 or c >= n or grid[r][c] == '#':
break
cnt += 1

return cnt

# Known winning states.
for r in range(m):
for c in range(n):
if grid[r][c] == '#':
continue

state = (f0, (r, c), CAT)
winners[state] = MOUSE
queue.append(state)

state = ((r, c), f0, MOUSE)
winners[state] = CAT
queue.append(state)

for moving in (MOUSE, CAT):
state = ((r, c), (r, c), moving)
winners[state] = CAT
queue.append(state)

initial = (m0, c0, MOUSE)
doubt_moves: dict[State, int] = {} # state -> number of remaining undetermined moves

while queue:
state = queue.popleft()
winner = winners[state]
if winner == state[2]:
for p_state in prev_states(*state):
if p_state in winners: continue
if p_state not in doubt_moves: doubt_moves[p_state] = move_cnt(*p_state)
doubt_moves[p_state] -= 1
if doubt_moves[p_state] == 0:
if p_state == initial: return winner == MOUSE
winners[p_state] = winner
queue.append(p_state)
else:
for p_state in prev_states(*state):
if p_state in winners: continue
if p_state == initial: return winner == MOUSE
winners[p_state] = winner
queue.append(p_state)

return False # DRAW