-
Notifications
You must be signed in to change notification settings - Fork 2
feat(algorithms, graphs): reconstruct itinerary #144
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
0d9a4ce
feat(algorithms, graphs): reconstruct itinerary
BrianLusina c8e3464
updating DIRECTORY.md
07f174e
feat(algorithms, graphs): cat and mouse
BrianLusina 303bcb7
updating DIRECTORY.md
4123342
refactor(algorithms, graphs): equality check
BrianLusina 13f59f7
docs(algorithms, graphs): hyphenate depth-first search
BrianLusina 5e0f104
docs(algorithms, graphs): fix typo
BrianLusina File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
|
||
|  | ||
|
|
||
| ```text | ||
| Input: graph = [[2,5],[3],[0,4,5],[1,4,5],[2,3],[0,2,3]] | ||
| Output: 0 | ||
| ``` | ||
|
|
||
|  | ||
| ```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). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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] |
Binary file added
BIN
+80.6 KB
algorithms/graphs/cat_and_mouse/images/examples/cat_and_mouse_example_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added
BIN
+50.3 KB
algorithms/graphs/cat_and_mouse/images/examples/cat_and_mouse_example_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
|
||
|  | ||
|  | ||
|  | ||
|
|
||
| ## 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: | ||
|
|
||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
|  | ||
|
|
||
| ### 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. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.