-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
236 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 %} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |