questions C
This commit is contained in:
296
backend/data/questions/cat-and-mouse.yaml
Normal file
296
backend/data/questions/cat-and-mouse.yaml
Normal file
@@ -0,0 +1,296 @@
|
||||
title: Cat and Mouse
|
||||
slug: cat-and-mouse
|
||||
difficulty: hard
|
||||
leetcode_id: 913
|
||||
leetcode_url: https://leetcode.com/problems/cat-and-mouse/
|
||||
categories:
|
||||
- graphs
|
||||
- dynamic-programming
|
||||
patterns:
|
||||
- dfs
|
||||
- dynamic-programming
|
||||
|
||||
description: |
|
||||
A game on an **undirected** graph is played by two players, Mouse and Cat, who alternate turns.
|
||||
|
||||
The graph is given as follows: `graph[a]` is a list of all nodes `b` such that `ab` is an edge of the graph.
|
||||
|
||||
The mouse starts at node `1` and goes first, the cat starts at node `2` and goes second, and there is a hole at node `0`.
|
||||
|
||||
During each player's turn, they **must** travel along one edge of the graph that meets where they are. For example, if the Mouse is at node `1`, it **must** travel to any node in `graph[1]`.
|
||||
|
||||
Additionally, it is not allowed for the Cat to travel to the Hole (node `0`).
|
||||
|
||||
Then, the game can end in three ways:
|
||||
|
||||
- If ever the Cat occupies the same node as the Mouse, the Cat wins.
|
||||
- If ever the Mouse reaches the Hole, the Mouse wins.
|
||||
- If ever a position is repeated (i.e., the players are in the same position as a previous turn, and it is the same player's turn to move), the game is a draw.
|
||||
|
||||
Given a `graph`, and assuming both players play optimally, return:
|
||||
- `1` if the mouse wins the game,
|
||||
- `2` if the cat wins the game, or
|
||||
- `0` if the game is a draw.
|
||||
|
||||
constraints: |
|
||||
- `3 <= graph.length <= 50`
|
||||
- `1 <= graph[i].length < graph.length`
|
||||
- `0 <= graph[i][j] < graph.length`
|
||||
- `graph[i][j] != i`
|
||||
- `graph[i]` is unique
|
||||
- The mouse and the cat can always move
|
||||
|
||||
examples:
|
||||
- input: "graph = [[2,5],[3],[0,4,5],[1,4,5],[2,3],[0,2,3]]"
|
||||
output: "0"
|
||||
explanation: "With optimal play from both sides, the game ends in a draw. Neither the mouse can guarantee reaching the hole, nor can the cat guarantee catching the mouse."
|
||||
- input: "graph = [[1,3],[0],[3],[0,2]]"
|
||||
output: "1"
|
||||
explanation: "The mouse can reach the hole (node 0) before the cat can catch it, so the mouse wins."
|
||||
|
||||
explanation:
|
||||
intuition: |
|
||||
Imagine a chess match where both players can see every possible future move. This is a **two-player zero-sum game with perfect information** — what's good for the mouse is bad for the cat, and vice versa.
|
||||
|
||||
The key insight is that the game state can be fully described by three pieces of information: **where the mouse is**, **where the cat is**, and **whose turn it is**. From any state, we can determine the outcome if both players play optimally.
|
||||
|
||||
Think of it like this: the mouse is trying to reach the hole (node `0`) while avoiding the cat. The cat is trying to catch the mouse while being forbidden from entering the hole. Each player, on their turn, will choose the move that gives them the best possible outcome.
|
||||
|
||||
This is a classic application of **game theory** and **minimax reasoning**:
|
||||
- On the mouse's turn, it picks the move that maximises its chances of winning (or at least drawing)
|
||||
- On the cat's turn, it picks the move that maximises its chances of winning
|
||||
|
||||
We use **dynamic programming with memoisation** to avoid recalculating the same game states. The tricky part is handling draws — if we revisit a state, the game cycles forever.
|
||||
|
||||
approach: |
|
||||
We solve this using **Memoised DFS with Game State Tracking**:
|
||||
|
||||
**Step 1: Define the game state**
|
||||
|
||||
- `mouse`: Current position of the mouse (node index)
|
||||
- `cat`: Current position of the cat (node index)
|
||||
- `turn`: Whose turn it is (`0` for mouse, `1` for cat)
|
||||
|
||||
|
||||
|
||||
**Step 2: Identify base cases**
|
||||
|
||||
- If `mouse == 0`: Mouse wins (reached the hole) — return `1`
|
||||
- If `mouse == cat`: Cat wins (caught the mouse) — return `2`
|
||||
- If we've seen this state before or exceeded `2n` moves: Draw — return `0`
|
||||
|
||||
|
||||
|
||||
**Step 3: Apply minimax logic**
|
||||
|
||||
- **Mouse's turn**: The mouse tries each adjacent node and picks the best outcome
|
||||
- If any move leads to mouse winning, return `1`
|
||||
- If no winning move but a draw exists, return `0`
|
||||
- Otherwise, return `2` (cat wins)
|
||||
|
||||
- **Cat's turn**: The cat tries each adjacent node (except node `0`) and picks the best outcome
|
||||
- If any move leads to cat winning, return `2`
|
||||
- If no winning move but a draw exists, return `0`
|
||||
- Otherwise, return `1` (mouse wins)
|
||||
|
||||
|
||||
|
||||
**Step 4: Memoise results**
|
||||
|
||||
- Cache the result for each `(mouse, cat, turn)` state
|
||||
- This prevents exponential recalculation
|
||||
|
||||
|
||||
|
||||
The key to handling draws is limiting recursion depth to `2n` turns. If the game hasn't ended by then, it must be cycling through states, which means a draw.
|
||||
|
||||
common_pitfalls:
|
||||
- title: Forgetting the Cat Cannot Enter the Hole
|
||||
description: |
|
||||
The cat is forbidden from moving to node `0` (the hole). When iterating through the cat's possible moves, you must skip node `0`.
|
||||
|
||||
Forgetting this constraint leads to incorrect results where the cat might "block" the hole.
|
||||
wrong_approach: "Allowing cat to move to any adjacent node"
|
||||
correct_approach: "Filter out node 0 from cat's valid moves"
|
||||
|
||||
- title: Infinite Recursion Without Cycle Detection
|
||||
description: |
|
||||
Without proper handling, the recursion can loop forever through repeating states. For example, mouse and cat might chase each other in circles.
|
||||
|
||||
The solution is to limit the recursion depth to `2n` moves (where `n` is the number of nodes). After this many moves without a winner, the game must be in a cycle, meaning it's a draw.
|
||||
wrong_approach: "Relying only on visited set without depth limit"
|
||||
correct_approach: "Limit recursion to 2n turns and return draw if exceeded"
|
||||
|
||||
- title: Incorrect Minimax Logic
|
||||
description: |
|
||||
A common mistake is not correctly implementing the minimax principle:
|
||||
- The mouse should return "mouse wins" if **any** move leads to a mouse win
|
||||
- The cat should return "cat wins" if **any** move leads to a cat win
|
||||
|
||||
Getting this backwards or not properly tracking "can draw" vs "must lose" leads to wrong answers.
|
||||
wrong_approach: "Returning immediately on first move result"
|
||||
correct_approach: "Evaluate all moves to find best possible outcome"
|
||||
|
||||
key_takeaways:
|
||||
- "**Game theory problems**: Use minimax — each player picks their best move assuming the opponent plays optimally"
|
||||
- "**State representation**: The state is `(mouse_pos, cat_pos, turn)` — memoising this avoids exponential blowup"
|
||||
- "**Cycle detection**: Limit recursion depth to detect draws; `2n` moves is sufficient since each state involves at most `n^2` configurations"
|
||||
- "**Graph games**: Many game problems on graphs use similar DFS + memoisation patterns"
|
||||
|
||||
time_complexity: "O(n^3). There are O(n^2) states (mouse position x cat position x turn), and each state explores O(n) neighbours."
|
||||
space_complexity: "O(n^2). The memoisation cache stores results for all `(mouse, cat, turn)` combinations."
|
||||
|
||||
solutions:
|
||||
- approach_name: Memoised DFS (Minimax)
|
||||
is_optimal: true
|
||||
code: |
|
||||
def cat_mouse_game(graph: list[list[int]]) -> int:
|
||||
n = len(graph)
|
||||
# Constants for game outcomes
|
||||
DRAW, MOUSE_WIN, CAT_WIN = 0, 1, 2
|
||||
|
||||
# Memoisation cache: (mouse, cat, turn) -> result
|
||||
memo = {}
|
||||
|
||||
def dfs(mouse: int, cat: int, turn: int, moves: int) -> int:
|
||||
# Base case: too many moves means a draw (cycle detected)
|
||||
if moves >= 2 * n:
|
||||
return DRAW
|
||||
|
||||
# Mouse reached the hole - mouse wins
|
||||
if mouse == 0:
|
||||
return MOUSE_WIN
|
||||
|
||||
# Cat caught the mouse - cat wins
|
||||
if mouse == cat:
|
||||
return CAT_WIN
|
||||
|
||||
# Check memo cache
|
||||
state = (mouse, cat, turn)
|
||||
if state in memo:
|
||||
return memo[state]
|
||||
|
||||
if turn == 0: # Mouse's turn
|
||||
# Mouse tries to win, or at least draw
|
||||
can_draw = False
|
||||
for next_mouse in graph[mouse]:
|
||||
result = dfs(next_mouse, cat, 1, moves + 1)
|
||||
if result == MOUSE_WIN:
|
||||
memo[state] = MOUSE_WIN
|
||||
return MOUSE_WIN
|
||||
if result == DRAW:
|
||||
can_draw = True
|
||||
# Mouse couldn't win; return draw if possible, else cat wins
|
||||
memo[state] = DRAW if can_draw else CAT_WIN
|
||||
|
||||
else: # Cat's turn
|
||||
# Cat tries to win, or at least draw
|
||||
can_draw = False
|
||||
for next_cat in graph[cat]:
|
||||
# Cat cannot move to the hole
|
||||
if next_cat == 0:
|
||||
continue
|
||||
result = dfs(mouse, next_cat, 0, moves + 1)
|
||||
if result == CAT_WIN:
|
||||
memo[state] = CAT_WIN
|
||||
return CAT_WIN
|
||||
if result == DRAW:
|
||||
can_draw = True
|
||||
# Cat couldn't win; return draw if possible, else mouse wins
|
||||
memo[state] = DRAW if can_draw else MOUSE_WIN
|
||||
|
||||
return memo[state]
|
||||
|
||||
# Start: mouse at 1, cat at 2, mouse's turn (0), 0 moves made
|
||||
return dfs(1, 2, 0, 0)
|
||||
explanation: |
|
||||
**Time Complexity:** O(n^3) — There are O(n^2) unique states, each exploring O(n) edges.
|
||||
|
||||
**Space Complexity:** O(n^2) — Memoisation cache for all states plus O(n) recursion depth.
|
||||
|
||||
This solution uses depth-first search with memoisation to explore the game tree. The minimax logic ensures each player picks their optimal move. The `moves` counter prevents infinite loops by detecting cycles after `2n` moves.
|
||||
|
||||
- approach_name: BFS with Coloring (Bottom-Up)
|
||||
is_optimal: true
|
||||
code: |
|
||||
from collections import deque
|
||||
|
||||
def cat_mouse_game(graph: list[list[int]]) -> int:
|
||||
n = len(graph)
|
||||
DRAW, MOUSE_WIN, CAT_WIN = 0, 1, 2
|
||||
|
||||
# color[mouse][cat][turn] = outcome
|
||||
color = [[[DRAW] * 2 for _ in range(n)] for _ in range(n)]
|
||||
# degree[mouse][cat][turn] = number of unprocessed children
|
||||
degree = [[[0] * 2 for _ in range(n)] for _ in range(n)]
|
||||
|
||||
# Initialise degrees (how many moves each player can make)
|
||||
for mouse in range(n):
|
||||
for cat in range(n):
|
||||
degree[mouse][cat][0] = len(graph[mouse]) # Mouse moves
|
||||
degree[mouse][cat][1] = len(graph[cat]) # Cat moves
|
||||
# Cat can't go to hole, so reduce cat's degree
|
||||
if 0 in graph[cat]:
|
||||
degree[mouse][cat][1] -= 1
|
||||
|
||||
# Queue of known states: (mouse, cat, turn, winner)
|
||||
queue = deque()
|
||||
|
||||
# Base cases: cat wins when cat == mouse (except at hole)
|
||||
for cat in range(1, n):
|
||||
for turn in range(2):
|
||||
color[cat][cat][turn] = CAT_WIN
|
||||
queue.append((cat, cat, turn, CAT_WIN))
|
||||
|
||||
# Base cases: mouse wins when mouse reaches hole
|
||||
for cat in range(1, n):
|
||||
for turn in range(2):
|
||||
color[0][cat][turn] = MOUSE_WIN
|
||||
queue.append((0, cat, turn, MOUSE_WIN))
|
||||
|
||||
# Process queue, propagating known results backwards
|
||||
while queue:
|
||||
mouse, cat, turn, result = queue.popleft()
|
||||
|
||||
# Find parent states that lead to this state
|
||||
if turn == 0: # Current is mouse's turn, parent was cat's turn
|
||||
for prev_cat in graph[cat]:
|
||||
if prev_cat == 0: # Cat can't be at hole
|
||||
continue
|
||||
if color[mouse][prev_cat][1] != DRAW:
|
||||
continue # Already determined
|
||||
if result == CAT_WIN:
|
||||
# Cat found a winning move
|
||||
color[mouse][prev_cat][1] = CAT_WIN
|
||||
queue.append((mouse, prev_cat, 1, CAT_WIN))
|
||||
else:
|
||||
# This child doesn't help cat; decrement degree
|
||||
degree[mouse][prev_cat][1] -= 1
|
||||
if degree[mouse][prev_cat][1] == 0:
|
||||
# All moves lead to mouse win/draw -> mouse wins
|
||||
color[mouse][prev_cat][1] = MOUSE_WIN
|
||||
queue.append((mouse, prev_cat, 1, MOUSE_WIN))
|
||||
else: # Current is cat's turn, parent was mouse's turn
|
||||
for prev_mouse in graph[mouse]:
|
||||
if color[prev_mouse][cat][0] != DRAW:
|
||||
continue # Already determined
|
||||
if result == MOUSE_WIN:
|
||||
# Mouse found a winning move
|
||||
color[prev_mouse][cat][0] = MOUSE_WIN
|
||||
queue.append((prev_mouse, cat, 0, MOUSE_WIN))
|
||||
else:
|
||||
# This child doesn't help mouse; decrement degree
|
||||
degree[prev_mouse][cat][0] -= 1
|
||||
if degree[prev_mouse][cat][0] == 0:
|
||||
# All moves lead to cat win/draw -> cat wins
|
||||
color[prev_mouse][cat][0] = CAT_WIN
|
||||
queue.append((prev_mouse, cat, 0, CAT_WIN))
|
||||
|
||||
# Return result for initial state: mouse at 1, cat at 2, mouse's turn
|
||||
return color[1][2][0]
|
||||
explanation: |
|
||||
**Time Complexity:** O(n^3) — We process each of the O(n^2) states at most once, with O(n) transitions.
|
||||
|
||||
**Space Complexity:** O(n^2) — Storage for the `color` and `degree` arrays.
|
||||
|
||||
This bottom-up approach works backwards from known terminal states. We start with states where we know the outcome (mouse at hole = mouse wins, cat catches mouse = cat wins) and propagate results backwards using the degree-counting technique. States that never get assigned a winner remain as draws.
|
||||
Reference in New Issue
Block a user