Skip to content

Commit 07f174e

Browse files
committed
feat(algorithms, graphs): cat and mouse
1 parent c8e3464 commit 07f174e

File tree

5 files changed

+196
-0
lines changed

5 files changed

+196
-0
lines changed
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# Cat and Mouse
2+
3+
A game on an undirected graph is played by two players, Mouse and Cat, who alternate turns.
4+
5+
The graph is given as follows: graph[a] is a list of all nodes b such that ab is an edge of the graph.
6+
7+
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.
8+
9+
During each player's turn, they must travel along one edge of the graph that meets where they are. For example, if the
10+
Mouse is at node 1, it must travel to any node in graph[1].
11+
12+
Additionally, it is not allowed for the Cat to travel to the Hole (node 0).
13+
14+
Then, the game can end in three ways:
15+
16+
- If ever the Cat occupies the same node as the Mouse, the Cat wins.
17+
- If ever the Mouse reaches the Hole, the Mouse wins.
18+
- If ever a position is repeated (i.e., the players are in the same position as a previous turn, and it is the same
19+
- player's turn to move), the game is a draw.
20+
Given a graph, and assuming both players play optimally, return
21+
22+
- 1 if the mouse wins the game,
23+
- 2 if the cat wins the game, or
24+
- 0 if the game is a draw.
25+
26+
## Examples
27+
28+
![Example 1](./images/examples/cat_and_mouse_example_1.png)
29+
30+
```text
31+
Input: graph = [[2,5],[3],[0,4,5],[1,4,5],[2,3],[0,2,3]]
32+
Output: 0
33+
```
34+
35+
![Example 2](./images/examples/cat_and_mouse_example_2.png)
36+
```text
37+
Input: graph = [[1,3],[0],[3],[0,2]]
38+
Output: 1
39+
```
40+
41+
## Constraints
42+
43+
- 3 <= graph.length <= 50
44+
- 1 <= graph[i].length < graph.length
45+
- 0 <= graph[i][j] < graph.length
46+
- graph[i][j] != i
47+
- graph[i] is unique.
48+
- The mouse and the cat can always move.
49+
50+
## Related Topics
51+
52+
- Math
53+
- Dynamic Programming
54+
- Graph
55+
- Topological Sort
56+
- Memoization
57+
- Game Theory
58+
59+
## Solution
60+
61+
### Minimax/Percolate from Resolved States
62+
63+
The state of the game can be represented as (m, c, t) where m is the location of the mouse, c is the location of the
64+
cat, and t is 1 if it is the mouse's move, else 2. Let's call these states nodes. These states form a directed graph:
65+
the player whose turn it is has various moves which can be considered as outgoing edges from this node to other nodes.
66+
67+
Some of these nodes are already resolved: if the mouse is at the hole (m = 0), then the mouse wins; if the cat is where
68+
the mouse is (c = m), then the cat wins. Let's say that nodes will either be colored MOUSE, CAT, or DRAW depending on
69+
which player is assured victory.
70+
71+
As in a standard minimax algorithm, the Mouse player will prefer MOUSE nodes first, DRAW nodes second, and CAT nodes
72+
last, and the Cat player prefers these nodes in the opposite order.
73+
74+
#### Algorithm
75+
76+
We will color each node marked DRAW according to the following rule. (We'll suppose the node has node.turn = Mouse: the
77+
other case is similar.)
78+
79+
- ("Immediate coloring"): If there is a child that is colored MOUSE, then this node will also be colored MOUSE.
80+
- ("Eventual coloring"): If all children are colored CAT, then this node will also be colored CAT.
81+
82+
We will repeatedly do this kind of coloring until no node satisfies the above conditions. To perform this coloring
83+
efficiently, we will use a queue and perform a bottom-up percolation:
84+
85+
- Enqueue any node initially colored (because the Mouse is at the Hole, or the Cat is at the Mouse.)
86+
- For every node in the queue, for each parent of that node:
87+
- Do an immediate coloring of parent if you can.
88+
- If you can't, then decrement the side-count of the number of children marked DRAW. If it becomes zero, then do an
89+
"eventual coloring" of this parent.
90+
- All parents that were colored in this manner get enqueued to the queue.
91+
92+
#### Proof of Correctness
93+
94+
Our proof is similar to a proof that minimax works.
95+
96+
Say we cannot color any nodes any more, and say from any node colored CAT or MOUSE we need at most K moves to win. If
97+
say, some node marked DRAW is actually a win for Mouse, it must have been with >K moves. Then, a path along optimal play
98+
(that tries to prolong the loss as long as possible) must arrive at a node colored MOUSE
99+
(as eventually the Mouse reaches the Hole.) Thus, there must have been some transition DRAW→MOUSE along this path.
100+
101+
If this transition occurred at a node with node.turn = Mouse, then it breaks our immediate coloring rule. If it occured
102+
with node.turn = Cat, and all children of node have color MOUSE, then it breaks our eventual coloring rule. If some child
103+
has color CAT, then it breaks our immediate coloring rule. Thus, in this case node will have some child with DRAW, which
104+
breaks our optimal play assumption, as moving to this child ends the game in >K moves, whereas moving to the colored
105+
neighbor ends the game in ≤K moves.
106+
107+
#### Complexity Analysis
108+
109+
##### Time Complexity
110+
111+
O(N^3), where N is the number of nodes in the graph. There are O(N^2) states, and each state has an
112+
outdegree of N, as there are at most N different moves.
113+
114+
##### Space Complexity
115+
116+
O(N^2).
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from typing import List, Deque, DefaultDict, Tuple
2+
from collections import deque, defaultdict
3+
4+
5+
def cat_mouse_game(graph: List[List[int]]) -> int:
6+
n = len(graph)
7+
8+
# Final game states that determine which player wins
9+
draw, mouse, cat = 0, 1, 2
10+
color: DefaultDict[Tuple[int, int, int], int] = defaultdict(int)
11+
12+
# What nodes could play their turn to
13+
# arrive at node (m, c, t) ?
14+
def parents(m, c, t):
15+
if t == 2:
16+
for m2 in graph[m]:
17+
yield m2, c, 3 - t
18+
else:
19+
for c2 in graph[c]:
20+
if c2:
21+
yield m, c2, 3 - t
22+
23+
# degree[node] : the number of neutral children of this node
24+
degree = {}
25+
for m in range(n):
26+
for c in range(n):
27+
degree[m, c, 1] = len(graph[m])
28+
degree[m, c, 2] = len(graph[c]) - (0 in graph[c])
29+
30+
# enqueued : all nodes that are colored
31+
queue: Deque[Tuple[int, int, int, int]] = deque([])
32+
for i in range(n):
33+
for t in range(1, 3):
34+
color[0, i, t] = mouse
35+
queue.append((0, i, t, mouse))
36+
if i > 0:
37+
color[i, i, t] = cat
38+
queue.append((i, i, t, cat))
39+
40+
# percolate
41+
while queue:
42+
# for nodes that are colored :
43+
i, j, t, c = queue.popleft()
44+
# for every parent of this node i, j, t :
45+
for i2, j2, t2 in parents(i, j, t):
46+
# if this parent is not colored :
47+
if color[i2, j2, t2] is draw:
48+
# if the parent can make a winning move (ie. mouse to MOUSE), do so
49+
if t2 == c: # winning move
50+
color[i2, j2, t2] = c
51+
queue.append((i2, j2, t2, c))
52+
# else, this parent has degree[parent]--, and enqueue if all children
53+
# of this parent are colored as losing moves
54+
else:
55+
degree[i2, j2, t2] -= 1
56+
if degree[i2, j2, t2] == 0:
57+
color[i2, j2, t2] = 3 - t2
58+
queue.append((i2, j2, t2, 3 - t2))
59+
60+
return color[1, 2, 1]
80.6 KB
Loading
50.3 KB
Loading
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import unittest
2+
from typing import List
3+
from parameterized import parameterized
4+
from algorithms.graphs.cat_and_mouse import cat_mouse_game
5+
6+
CAT_AND_MOUSE_GAME_TESTS = [
7+
([[2, 5], [3], [0, 4, 5], [1, 4, 5], [2, 3], [0, 2, 3]], 0),
8+
([[1, 3], [0], [3], [0, 2]], 1),
9+
]
10+
11+
12+
class CatAndMouseGameTestCase(unittest.TestCase):
13+
@parameterized.expand(CAT_AND_MOUSE_GAME_TESTS)
14+
def test_cat_and_mouse_game(self, graph: List[List[int]], expected: int):
15+
actual = cat_mouse_game(graph)
16+
self.assertEqual(expected, actual)
17+
18+
19+
if __name__ == "__main__":
20+
unittest.main()

0 commit comments

Comments
 (0)