Skip to content

Commit

Permalink
add(coding/1728): cat-and-mouse-ii
Browse files Browse the repository at this point in the history
  • Loading branch information
calfzhou committed Feb 11, 2025
1 parent aae04a3 commit bf64abf
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 0 deletions.
102 changes: 102 additions & 0 deletions source/coding/1728-cat-and-mouse-ii.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
title: 1728. Cat and Mouse II
notebook: coding
tags:
- hard
date: 2025-02-11 16:36:37
updated: 2025-02-11 16:36:37
---
## 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:**

{% invert %}
{% image 1728-cat-and-mouse-ii/case1.png width:580px %}
{% endinvert %}

> 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:**

{% invert %}
{% image 1728-cat-and-mouse-ii/case2.png width:580px %}
{% endinvert %}

> 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

``` python
class Solution:
def canMouseWin(self, grid: List[str], catJump: int, mouseJump: int) -> bool:
```

{% asset_code coding/1728-cat-and-mouse-ii/solution_test.py %}

## Thoughts

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

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

直接套用 [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

{% asset_code coding/1728-cat-and-mouse-ii/solution.py %}
Binary file added source/coding/1728-cat-and-mouse-ii/case1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added source/coding/1728-cat-and-mouse-ii/case2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
119 changes: 119 additions & 0 deletions source/coding/1728-cat-and-mouse-ii/solution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,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
15 changes: 15 additions & 0 deletions source/coding/1728-cat-and-mouse-ii/solution_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,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

0 comments on commit bf64abf

Please sign in to comment.