GoCalf Calf 的自留地 · 昙花一现

1728. Cat and Mouse II

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:

case1
case1

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:

case2
case2

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:
solution_test.py下载完整源码
py
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

py
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

评论需要 JavaScript。