Skip to content

Commit 8c30b66

Browse files
authored
Merge pull request #144 from BrianLusina/feat/algorithms-graphs
feat(algorithms, graphs): reconstruct itinerary
2 parents 9b8a6d4 + 5e0f104 commit 8c30b66

27 files changed

+614
-2
lines changed

DIRECTORY.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@
8787
* Happy Number
8888
* [Test Happy Number](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/fast_and_slow/happy_number/test_happy_number.py)
8989
* Graphs
90+
* Cat And Mouse
91+
* [Test Cat And Mouse](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/cat_and_mouse/test_cat_and_mouse.py)
9092
* Course Schedule
9193
* [Test Course Schedule](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/course_schedule/test_course_schedule.py)
9294
* Evaluate Division
@@ -108,6 +110,8 @@
108110
* [Union Find](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/number_of_islands/union_find.py)
109111
* Number Of Provinces
110112
* [Test Number Of Provinces](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/number_of_provinces/test_number_of_provinces.py)
113+
* Reconstruct Itinerary
114+
* [Test Reconstruct Itinerary](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/reconstruct_itinerary/test_reconstruct_itinerary.py)
111115
* Reorder Routes
112116
* [Test Reorder Routes](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/reorder_routes/test_reorder_routes.py)
113117
* Rotting Oranges
@@ -321,6 +325,8 @@
321325
* [Linked List Utils](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/linked_list_utils.py)
322326
* Mergeklinkedlists
323327
* [Test Merge](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/mergeklinkedlists/test_merge.py)
328+
* Reorder List
329+
* [Test Reorder List](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/reorder_list/test_reorder_list.py)
324330
* Singly Linked List
325331
* [Node](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/singly_linked_list/node.py)
326332
* [Single Linked List](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/singly_linked_list/single_linked_list.py)
@@ -961,8 +967,6 @@
961967
* [Test Longest Consecutive Sequence](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_longest_consecutive_sequence.py)
962968
* [Test Max Subarray](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_max_subarray.py)
963969
* [Test Zig Zag Sequence](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_zig_zag_sequence.py)
964-
* Linked List
965-
* [Test Reorder List](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/linked_list/test_reorder_list.py)
966970
* [Test Build One 2 3](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/test_build_one_2_3.py)
967971
* [Test Consecutive](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/test_consecutive.py)
968972
* [Test Data Reverse](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/test_data_reverse.py)
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] == 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()
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# Reconstruct Itinerary
2+
3+
Given a list of airline tickets where tickets[i] = [fromi, toi] represent a departure airport and an arrival airport of
4+
a single flight, reconstruct the itinerary in the correct order and return it.
5+
6+
The person who owns these tickets always starts their journey from "JFK". Therefore, the itinerary must begin with "JFK".
7+
If there are multiple valid itineraries, you should prioritize the one with the smallest lexical order when considering
8+
a single string.
9+
10+
> Lexicographical order is a way of sorting similar to how words are arranged in a dictionary. It compares items
11+
> character by character, based on their order in the alphabet or numerical value.
12+
13+
- For example, the itinerary ["JFK", "EDU"] has a smaller lexical order than ["JFK", "EDX"].
14+
15+
> Note: You may assume all tickets form at least one valid itinerary. You must use all the tickets exactly once.
16+
17+
## Constraints
18+
19+
- 1 <= `tickets.length` <= 300
20+
- `tickets[i].length` == 2
21+
- `fromi.length` == 3
22+
- `toi.length` == 3
23+
- `fromi` != `toi`
24+
- `fromi` and `toi` consist of upper case English letters
25+
26+
## Examples
27+
28+
![Example 1](./images/examples/reconstruct_itinerary_example_1.png)
29+
![Example 2](./images/examples/reconstruct_itinerary_example_2.png)
30+
![Example 3](./images/examples/reconstruct_itinerary_example_3.png)
31+
32+
## Related Topics
33+
34+
- Depth-First Search
35+
- Graph
36+
- Eulerian Circuit
37+
38+
## Solution
39+
40+
The algorithm uses __Hierholzer’s algorithm__ to reconstruct travel itineraries from a list of airline tickets. This
41+
problem is like finding an __Eulerian path__ but with a fixed starting point, “JFK”. Hierholzer’s algorithm is great for
42+
finding Eulerian paths and cycles, which is why we use it here.
43+
44+
> Hierholzer's algorithm is a method for finding an Eulerian circuit (a cycle that visits every edge exactly once) in a
45+
> graph. It starts from any vertex and follows edges until it returns to the starting vertex, forming a cycle. If there
46+
> are any unvisited edges, it starts a new cycle from a vertex on the existing cycle that has unvisited edges and merges
47+
> the cycles. The process continues until all edges are visited.
48+
49+
> An Eulerian path is a trail in a graph that visits every edge exactly once. An Eulerian path can exist only if exactly
50+
> zero or two vertices have an odd degree. If there are exactly zero vertices with an odd degree, the path can form a
51+
> circuit (Eulerian circuit), where the starting and ending points are the same. If there are exactly two vertices with
52+
> an odd degree, the path starts at one of these vertices and ends at the other.
53+
54+
The algorithm starts by arranging the destinations in reverse lexicographical order to ensure we always choose the
55+
smallest destination first. It then uses depth-first search (DFS) starting from “JFK” to navigate the flights. As it
56+
explores each flight path, it builds the itinerary by appending each visited airport when there are no more destinations
57+
to visit from that airport. Since the airports are added in reverse order during this process, the final step is to
58+
reverse the list to get the correct itinerary.
59+
60+
The basic algorithm to solve this problem will be:
61+
62+
1. Create a dictionary, `flight_map`, to store the flight information. Each key represents an airport; its corresponding
63+
value is a list of destinations from that airport.
64+
2. Initialize an empty list, result, to store the reconstructed itinerary.
65+
3. Sort the destinations lexicographically in reverse order to ensure that the smallest destination is chosen first.
66+
4. Perform DFS traversal starting from the airport "JFK".
67+
- Get the list of destinations for the current airport from flight_map.
68+
- While there are destinations available:
69+
- Pop the next_destination from destinations.
70+
- Recursively explore all available flights starting from the popped next_destination, until all possible flights
71+
have been considered.
72+
- Append the current airport to the result list.
73+
5. Return the result list in reverse order to ensure the itinerary starts from the initial airport, "JFK", and proceeds
74+
through the subsequent airports in the correct order.
75+
76+
Let’s look at the following illustration to get a better understanding of the solution:
77+
78+
![Solution 1](./images/solutions/reconstruct_itinerary_solution_1.png)
79+
![Solution 2](./images/solutions/reconstruct_itinerary_solution_2.png)
80+
![Solution 3](./images/solutions/reconstruct_itinerary_solution_3.png)
81+
![Solution 4](./images/solutions/reconstruct_itinerary_solution_4.png)
82+
![Solution 5](./images/solutions/reconstruct_itinerary_solution_5.png)
83+
![Solution 6](./images/solutions/reconstruct_itinerary_solution_6.png)
84+
![Solution 7](./images/solutions/reconstruct_itinerary_solution_7.png)
85+
![Solution 8](./images/solutions/reconstruct_itinerary_solution_8.png)
86+
![Solution 9](./images/solutions/reconstruct_itinerary_solution_9.png)
87+
![Solution 10](./images/solutions/reconstruct_itinerary_solution_10.png)
88+
![Solution 11](./images/solutions/reconstruct_itinerary_solution_11.png)
89+
![Solution 12](./images/solutions/reconstruct_itinerary_solution_12.png)
90+
![Solution 13](./images/solutions/reconstruct_itinerary_solution_13.png)
91+
![Solution 14](./images/solutions/reconstruct_itinerary_solution_14.png)
92+
![Solution 15](./images/solutions/reconstruct_itinerary_solution_15.png)
93+
94+
### Time Complexity
95+
96+
Each edge (flight) is traversed once during the DFS process in the algorithm, resulting in a complexity proportional to
97+
the number of edges, ∣E∣.
98+
Before DFS, the outgoing edges for each airport must be sorted. The sorting operation’s complexity depends on the input
99+
graph’s structure.
100+
In the worst-case scenario, such as a highly unbalanced graph (e.g., star-shaped), where one airport (e.g., JFK)
101+
dominates the majority of flights, the sorting operation on this airport becomes highly expensive, possibly reaching N log N
102+
complexity where `N = |E|/2` In a more balanced or average scenario, where each airport has a roughly equal number of
103+
outgoing flights, the sorting operation complexity remains O(N log N) where N represents half of the total number of
104+
edges divided by twice the number of airports O(|E|/2|V|). Thus, the algorithm’s overall complexity is O(|E|log|E/2|),
105+
emphasizing the significance of the sorting operation in determining its performance.
106+
107+
### Space Complexity
108+
109+
The space complexity is O(∣V∣+∣E∣), where ∣V∣ is the number of airports and ∣E∣ is the number of flights.

0 commit comments

Comments
 (0)