Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions DIRECTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@
* Happy Number
* [Test Happy Number](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/fast_and_slow/happy_number/test_happy_number.py)
* Graphs
* Cat And Mouse
* [Test Cat And Mouse](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/cat_and_mouse/test_cat_and_mouse.py)
* Course Schedule
* [Test Course Schedule](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/course_schedule/test_course_schedule.py)
* Evaluate Division
Expand All @@ -108,6 +110,8 @@
* [Union Find](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/number_of_islands/union_find.py)
* Number Of Provinces
* [Test Number Of Provinces](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/number_of_provinces/test_number_of_provinces.py)
* Reconstruct Itinerary
* [Test Reconstruct Itinerary](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/reconstruct_itinerary/test_reconstruct_itinerary.py)
* Reorder Routes
* [Test Reorder Routes](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/reorder_routes/test_reorder_routes.py)
* Rotting Oranges
Expand Down Expand Up @@ -321,6 +325,8 @@
* [Linked List Utils](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/linked_list_utils.py)
* Mergeklinkedlists
* [Test Merge](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/mergeklinkedlists/test_merge.py)
* Reorder List
* [Test Reorder List](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/reorder_list/test_reorder_list.py)
* Singly Linked List
* [Node](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/singly_linked_list/node.py)
* [Single Linked List](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/singly_linked_list/single_linked_list.py)
Expand Down Expand Up @@ -961,8 +967,6 @@
* [Test Longest Consecutive Sequence](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_longest_consecutive_sequence.py)
* [Test Max Subarray](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_max_subarray.py)
* [Test Zig Zag Sequence](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_zig_zag_sequence.py)
* Linked List
* [Test Reorder List](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/linked_list/test_reorder_list.py)
* [Test Build One 2 3](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/test_build_one_2_3.py)
* [Test Consecutive](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/test_consecutive.py)
* [Test Data Reverse](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/test_data_reverse.py)
Expand Down
116 changes: 116 additions & 0 deletions algorithms/graphs/cat_and_mouse/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Cat and Mouse

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.

## Examples

![Example 1](./images/examples/cat_and_mouse_example_1.png)

```text
Input: graph = [[2,5],[3],[0,4,5],[1,4,5],[2,3],[0,2,3]]
Output: 0
```

![Example 2](./images/examples/cat_and_mouse_example_2.png)
```text
Input: graph = [[1,3],[0],[3],[0,2]]
Output: 1
```

## 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.

## Related Topics

- Math
- Dynamic Programming
- Graph
- Topological Sort
- Memoization
- Game Theory

## Solution

### Minimax/Percolate from Resolved States

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
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:
the player whose turn it is has various moves which can be considered as outgoing edges from this node to other nodes.

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
the mouse is (c = m), then the cat wins. Let's say that nodes will either be colored MOUSE, CAT, or DRAW depending on
which player is assured victory.

As in a standard minimax algorithm, the Mouse player will prefer MOUSE nodes first, DRAW nodes second, and CAT nodes
last, and the Cat player prefers these nodes in the opposite order.

#### Algorithm

We will color each node marked DRAW according to the following rule. (We'll suppose the node has node.turn = Mouse: the
other case is similar.)

- ("Immediate coloring"): If there is a child that is colored MOUSE, then this node will also be colored MOUSE.
- ("Eventual coloring"): If all children are colored CAT, then this node will also be colored CAT.

We will repeatedly do this kind of coloring until no node satisfies the above conditions. To perform this coloring
efficiently, we will use a queue and perform a bottom-up percolation:

- Enqueue any node initially colored (because the Mouse is at the Hole, or the Cat is at the Mouse.)
- For every node in the queue, for each parent of that node:
- Do an immediate coloring of parent if you can.
- If you can't, then decrement the side-count of the number of children marked DRAW. If it becomes zero, then do an
"eventual coloring" of this parent.
- All parents that were colored in this manner get enqueued to the queue.

#### Proof of Correctness

Our proof is similar to a proof that minimax works.

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
say, some node marked DRAW is actually a win for Mouse, it must have been with >K moves. Then, a path along optimal play
(that tries to prolong the loss as long as possible) must arrive at a node colored MOUSE
(as eventually the Mouse reaches the Hole.) Thus, there must have been some transition DRAW→MOUSE along this path.

If this transition occurred at a node with node.turn = Mouse, then it breaks our immediate coloring rule. If it occured
with node.turn = Cat, and all children of node have color MOUSE, then it breaks our eventual coloring rule. If some child
has color CAT, then it breaks our immediate coloring rule. Thus, in this case node will have some child with DRAW, which
breaks our optimal play assumption, as moving to this child ends the game in >K moves, whereas moving to the colored
neighbor ends the game in ≤K moves.

#### Complexity Analysis

##### Time Complexity

O(N^3), where N is the number of nodes in the graph. There are O(N^2) states, and each state has an
outdegree of N, as there are at most N different moves.

##### Space Complexity

O(N^2).
60 changes: 60 additions & 0 deletions algorithms/graphs/cat_and_mouse/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from typing import List, Deque, DefaultDict, Tuple
from collections import deque, defaultdict


def cat_mouse_game(graph: List[List[int]]) -> int:
n = len(graph)

# Final game states that determine which player wins
draw, mouse, cat = 0, 1, 2
color: DefaultDict[Tuple[int, int, int], int] = defaultdict(int)

# What nodes could play their turn to
# arrive at node (m, c, t) ?
def parents(m, c, t):
if t == 2:
for m2 in graph[m]:
yield m2, c, 3 - t
else:
for c2 in graph[c]:
if c2:
yield m, c2, 3 - t

# degree[node] : the number of neutral children of this node
degree = {}
for m in range(n):
for c in range(n):
degree[m, c, 1] = len(graph[m])
degree[m, c, 2] = len(graph[c]) - (0 in graph[c])

# enqueued : all nodes that are colored
queue: Deque[Tuple[int, int, int, int]] = deque([])
for i in range(n):
for t in range(1, 3):
color[0, i, t] = mouse
queue.append((0, i, t, mouse))
if i > 0:
color[i, i, t] = cat
queue.append((i, i, t, cat))

# percolate
while queue:
# for nodes that are colored :
i, j, t, c = queue.popleft()
# for every parent of this node i, j, t :
for i2, j2, t2 in parents(i, j, t):
# if this parent is not colored :
if color[i2, j2, t2] == draw:
# if the parent can make a winning move (ie. mouse to MOUSE), do so
if t2 == c: # winning move
color[i2, j2, t2] = c
queue.append((i2, j2, t2, c))
# else, this parent has degree[parent]--, and enqueue if all children
# of this parent are colored as losing moves
else:
degree[i2, j2, t2] -= 1
if degree[i2, j2, t2] == 0:
color[i2, j2, t2] = 3 - t2
queue.append((i2, j2, t2, 3 - t2))

return color[1, 2, 1]
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.
20 changes: 20 additions & 0 deletions algorithms/graphs/cat_and_mouse/test_cat_and_mouse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import unittest
from typing import List
from parameterized import parameterized
from algorithms.graphs.cat_and_mouse import cat_mouse_game

CAT_AND_MOUSE_GAME_TESTS = [
([[2, 5], [3], [0, 4, 5], [1, 4, 5], [2, 3], [0, 2, 3]], 0),
([[1, 3], [0], [3], [0, 2]], 1),
]


class CatAndMouseGameTestCase(unittest.TestCase):
@parameterized.expand(CAT_AND_MOUSE_GAME_TESTS)
def test_cat_and_mouse_game(self, graph: List[List[int]], expected: int):
actual = cat_mouse_game(graph)
self.assertEqual(expected, actual)


if __name__ == "__main__":
unittest.main()
109 changes: 109 additions & 0 deletions algorithms/graphs/reconstruct_itinerary/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Reconstruct Itinerary

Given a list of airline tickets where tickets[i] = [fromi, toi] represent a departure airport and an arrival airport of
a single flight, reconstruct the itinerary in the correct order and return it.

The person who owns these tickets always starts their journey from "JFK". Therefore, the itinerary must begin with "JFK".
If there are multiple valid itineraries, you should prioritize the one with the smallest lexical order when considering
a single string.

> Lexicographical order is a way of sorting similar to how words are arranged in a dictionary. It compares items
> character by character, based on their order in the alphabet or numerical value.

- For example, the itinerary ["JFK", "EDU"] has a smaller lexical order than ["JFK", "EDX"].

> Note: You may assume all tickets form at least one valid itinerary. You must use all the tickets exactly once.

## Constraints

- 1 <= `tickets.length` <= 300
- `tickets[i].length` == 2
- `fromi.length` == 3
- `toi.length` == 3
- `fromi` != `toi`
- `fromi` and `toi` consist of upper case English letters

## Examples

![Example 1](./images/examples/reconstruct_itinerary_example_1.png)
![Example 2](./images/examples/reconstruct_itinerary_example_2.png)
![Example 3](./images/examples/reconstruct_itinerary_example_3.png)

## Related Topics

- Depth-First Search
- Graph
- Eulerian Circuit

## Solution

The algorithm uses __Hierholzer’s algorithm__ to reconstruct travel itineraries from a list of airline tickets. This
problem is like finding an __Eulerian path__ but with a fixed starting point, “JFK”. Hierholzer’s algorithm is great for
finding Eulerian paths and cycles, which is why we use it here.

> Hierholzer's algorithm is a method for finding an Eulerian circuit (a cycle that visits every edge exactly once) in a
> graph. It starts from any vertex and follows edges until it returns to the starting vertex, forming a cycle. If there
> are any unvisited edges, it starts a new cycle from a vertex on the existing cycle that has unvisited edges and merges
> the cycles. The process continues until all edges are visited.

> An Eulerian path is a trail in a graph that visits every edge exactly once. An Eulerian path can exist only if exactly
> zero or two vertices have an odd degree. If there are exactly zero vertices with an odd degree, the path can form a
> circuit (Eulerian circuit), where the starting and ending points are the same. If there are exactly two vertices with
> an odd degree, the path starts at one of these vertices and ends at the other.

The algorithm starts by arranging the destinations in reverse lexicographical order to ensure we always choose the
smallest destination first. It then uses depth-first search (DFS) starting from “JFK” to navigate the flights. As it
explores each flight path, it builds the itinerary by appending each visited airport when there are no more destinations
to visit from that airport. Since the airports are added in reverse order during this process, the final step is to
reverse the list to get the correct itinerary.

The basic algorithm to solve this problem will be:

1. Create a dictionary, `flight_map`, to store the flight information. Each key represents an airport; its corresponding
value is a list of destinations from that airport.
2. Initialize an empty list, result, to store the reconstructed itinerary.
3. Sort the destinations lexicographically in reverse order to ensure that the smallest destination is chosen first.
4. Perform DFS traversal starting from the airport "JFK".
- Get the list of destinations for the current airport from flight_map.
- While there are destinations available:
- Pop the next_destination from destinations.
- Recursively explore all available flights starting from the popped next_destination, until all possible flights
have been considered.
- Append the current airport to the result list.
5. Return the result list in reverse order to ensure the itinerary starts from the initial airport, "JFK", and proceeds
through the subsequent airports in the correct order.

Let’s look at the following illustration to get a better understanding of the solution:

![Solution 1](./images/solutions/reconstruct_itinerary_solution_1.png)
![Solution 2](./images/solutions/reconstruct_itinerary_solution_2.png)
![Solution 3](./images/solutions/reconstruct_itinerary_solution_3.png)
![Solution 4](./images/solutions/reconstruct_itinerary_solution_4.png)
![Solution 5](./images/solutions/reconstruct_itinerary_solution_5.png)
![Solution 6](./images/solutions/reconstruct_itinerary_solution_6.png)
![Solution 7](./images/solutions/reconstruct_itinerary_solution_7.png)
![Solution 8](./images/solutions/reconstruct_itinerary_solution_8.png)
![Solution 9](./images/solutions/reconstruct_itinerary_solution_9.png)
![Solution 10](./images/solutions/reconstruct_itinerary_solution_10.png)
![Solution 11](./images/solutions/reconstruct_itinerary_solution_11.png)
![Solution 12](./images/solutions/reconstruct_itinerary_solution_12.png)
![Solution 13](./images/solutions/reconstruct_itinerary_solution_13.png)
![Solution 14](./images/solutions/reconstruct_itinerary_solution_14.png)
![Solution 15](./images/solutions/reconstruct_itinerary_solution_15.png)

### Time Complexity

Each edge (flight) is traversed once during the DFS process in the algorithm, resulting in a complexity proportional to
the number of edges, ∣E∣.
Before DFS, the outgoing edges for each airport must be sorted. The sorting operation’s complexity depends on the input
graph’s structure.
In the worst-case scenario, such as a highly unbalanced graph (e.g., star-shaped), where one airport (e.g., JFK)
dominates the majority of flights, the sorting operation on this airport becomes highly expensive, possibly reaching N log N
complexity where `N = |E|/2` In a more balanced or average scenario, where each airport has a roughly equal number of
outgoing flights, the sorting operation complexity remains O(N log N) where N represents half of the total number of
edges divided by twice the number of airports O(|E|/2|V|). Thus, the algorithm’s overall complexity is O(|E|log|E/2|),
emphasizing the significance of the sorting operation in determining its performance.

### Space Complexity

The space complexity is O(∣V∣+∣E∣), where ∣V∣ is the number of airports and ∣E∣ is the number of flights.
Loading
Loading