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
55 changes: 53 additions & 2 deletions algorithms/graphs/reorder_routes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ It's guaranteed that each city can reach city 0 after reorder.

Example 1

![](reorder_routes_example_1.png)
![](images/examples/reorder_routes_example_1.png)

Input: n = 6, connections = [[0,1],[1,3],[2,3],[4,0],[4,5]]
Output: 3
Explanation: Change the direction of edges show in red such that each node can reach the node 0 (capital).

Example 2

![](reorder_routes_example_2.png)
![](images/examples/reorder_routes_example_2.png)

Input: n = 5, connections = [[1,0],[1,2],[3,2],[3,4]]
Output: 2
Expand All @@ -33,6 +33,57 @@ Example 3
Input: n = 3, connections = [[1,0],[2,0]]
Output: 0

## Solution

This algorithm works because representing the network as a graph with directional information allows us to easily
identify which roads are misoriented. By performing a DFS from the capital (city 0), we traverse the entire network
exactly once. Every time we encounter a road directed away from city 0, we know it needs to be reversed. This approach
takes advantage of the network’s tree structure to ensure we only count the minimum number of necessary reversals.

Now, let’s look at the solution steps below:

1. Build an adjacency list using a variable graph to store the roads and their directional flags.
- For each connection [ai, bi] in connections:
- Append (bi, 1) to graph[ai], where the flag 1 indicates that the road goes from ai to bi and might need to be
reversed.
- Append (ai, 0) to graph[bi], where the flag 0 represents the reverse edge, which is correctly oriented for the
DFS.
2. Create a set, visited, to track which cities have been processed, preventing repeated visits.
3. Call dfs(0, graph, visited) to start the DFS from the capital (city 0) and store the result in a variable result.
4. Return result as the final answer is the minimum number of road reorientations needed.

**Define the DFS function**

1. Start the DFS from city 0
- Add the current city to the visited set.
- Initializes a variable, reversals, to count the number of road reversals needed for the subtree rooted at that city.
- For each neighbor and its associated flag (represented by need_reverse) in graph[city]:
- If the neighbor hasn’t been visited, add need_reverse to result (As need_reverse equals 1 if the road needs reversal).
- Recursively call dfs(neighbor, graph, visited) to continue the traversal.
- Returns reversals.

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

![Solution 1](./images/solutions/reorder_routes_solution_1.png)
![Solution 2](./images/solutions/reorder_routes_solution_2.png)
![Solution 3](./images/solutions/reorder_routes_solution_3.png)
![Solution 4](./images/solutions/reorder_routes_solution_4.png)
![Solution 5](./images/solutions/reorder_routes_solution_5.png)
![Solution 6](./images/solutions/reorder_routes_solution_6.png)
![Solution 7](./images/solutions/reorder_routes_solution_7.png)
![Solution 8](./images/solutions/reorder_routes_solution_8.png)
![Solution 9](./images/solutions/reorder_routes_solution_9.png)

### Time Complexity

The time complexity of the solution is O(n), where n is the number of cities because every node and its corresponding
edges are visited exactly once during the DFS.

### Space Complexity

The solution’s space complexity is O(n) due to the storage needed for the graph (adjacency list), the visited set, and
the recursion call stack.

## Related Topics

- Depth First Search
Expand Down
138 changes: 79 additions & 59 deletions algorithms/graphs/reorder_routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,85 @@
from collections import defaultdict


class Solution:
def min_reorder(n: int, connections: List[List[int]]) -> int:
"""Reorders the edges of the directed graph represented as an adjacency list in connections such that each vertex
is connected and has a path to the first vertex marked with 0. n represents the number of vertices.

Complexity;
n is the number of vertices/nodes

Time Complexity: O(n)
O(n) to initialize the adjacency list
The dfs function visits each node once, which takes O(n) time in total. Because we have undirected edges,
each edge can only be iterated twice (by nodes at the end), resulting in O(e) operations total while
visiting all nodes, where e is the number of edges. Because the given graph is a tree, there are n−1
undirected edges, so O(n+e)=O(n).

Space Complexity: O(n)
Building the adjacency list takes O(n) space.
The recursion call stack used by dfs can have no more than n elements in the worst-case scenario.
It would take up O(n) space in that case.

Args:
n(int): number of vertices or in this case, number of cities
connections(list): adjacency matrix for a directed graph or in this case, representation of cities
Returns:
int: minimum number of edges to re-arrange to ensure that each vertex is directly or indirectly connected to
the initial vertex
"""

# Adjacency list that contains list of pairs of nodes such that adj[node] contains all the neighbours of node in the
# form of [neighbour, sign] where neighbour is the neighbouring node and sign is the direction of the edge. If the
# sign is 0, it's an 'artificial' edge, meaning it was added by the algorithm in order to get to this vertex, and 1
# denotes that it's an 'original' edge, meaning that it's the original edge and no need to re-order that connection
adj: Dict[int, List[List[int]]] = defaultdict(lambda: [])

# keep track of number of reorders made
reorder_count = 0

def min_reorder(self, n: int, connections: List[List[int]]) -> int:
"""Reorders the edges of the directed graph represented as an adjacency list in connections such that each vertex
is connected and has a path to the first vertex marked with 0. n represents the number of vertices.

Complexity;
n is the number of vertices/nodes

Time Complexity: O(n)
O(n) to initialize the adjacency list
The dfs function visits each node once, which takes O(n) time in total. Because we have undirected edges,
each edge can only be iterated twice (by nodes at the end), resulting in O(e) operations total while
visiting all nodes, where e is the number of edges. Because the given graph is a tree, there are n−1
undirected edges, so O(n+e)=O(n).

Space Complexity: O(n)
Building the adjacency list takes O(n) space.
The recursion call stack used by dfs can have no more than n elements in the worst-case scenario.
It would take up O(n) space in that case.

Args:
n(int): number of vertices or in this case, number of cities
connections(list): adjacency matrix for a directed graph or in this case, representation of cities
Returns:
int: minimum number of edges to re-arrange to ensure that each vertex is directly or indirectly connected to
the initial vertex
"""

# Adjacency list that contains list of pairs of nodes such that adj[node] contains all the neighbours of node in the
# form of [neighbour, sign] where neighbour is the neighbouring node and sign is the direction of the edge. If the
# sign is 0, it's an 'artificial' edge, meaning it was added by the algorithm in order to get to this vertex, and 1
# denotes that it's an 'original' edge, meaning that it's the original edge and no need to re-order that connection
adj: Dict[int, List[List[int]]] = defaultdict(lambda: [])

def dfs(node: int, parent: int, adjacency: Dict[int, List[List[int]]]):
if node not in adjacency:
return

# iterate over all children of node(nodes that share an edge)
# for every child, sign, check if child is equal to parent. if child is equal to parent, we will not visit it
# again
# if child is not equal to parent, we perform count+=sign and recursively call the dfs with node = child and
# parent = node
for adjacent_node in adjacency.get(node):
child = adjacent_node[0]
sign = adjacent_node[1]

if child != parent:
self.reorder_count += sign
dfs(child, node, adjacency)

for connection in connections:
adj[connection[0]].append([connection[1], 1])
adj[connection[1]].append([connection[0], 0])

# we start with node, parent and the adjacency list as 0, and -1 and adj
dfs(0, -1, adj)

return self.reorder_count
def dfs(node: int, parent: int, adjacency: Dict[int, List[List[int]]]):
nonlocal reorder_count
if node not in adjacency:
return

# iterate over all children of node(nodes that share an edge)
# for every child, sign, check if child is equal to parent. if child is equal to parent, we will not visit it
# again
# if child is not equal to parent, we perform count+=sign and recursively call the dfs with node = child and
# parent = node
for adjacent_node in adjacency.get(node):
child = adjacent_node[0]
sign = adjacent_node[1]

if child != parent:
reorder_count += sign
dfs(child, node, adjacency)

for connection in connections:
adj[connection[0]].append([connection[1], 1])
adj[connection[1]].append([connection[0], 0])

# we start with node, parent and the adjacency list as 0, and -1 and adj
dfs(0, -1, adj)

return reorder_count


def min_reorder_2(n: int, connections: List[List[int]]) -> int:
def dfs(city, graph, visited):
visited.add(city)
reversals = 0
for neighbor, need_reverse in graph[city]:
if neighbor not in visited:
reversals += need_reverse
reversals += dfs(neighbor, graph, visited)
return reversals

graph = defaultdict(list)
for ai, bi in connections:
graph[ai].append((bi, 1))
graph[bi].append((ai, 0))

visited = set()
result = dfs(0, graph, visited)
return result
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.
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.
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.
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 18 additions & 23 deletions algorithms/graphs/reorder_routes/test_reorder_routes.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,25 @@
import unittest
from . import Solution
from typing import List
from parameterized import parameterized
from algorithms.graphs.reorder_routes import min_reorder

REORDER_ROUTES_TEST_CASES = [
(6, [[0, 1], [1, 3], [2, 3], [4, 0], [4, 5]], 3),
(5, [[1, 0], [1, 2], [3, 2], [3, 4]], 2),
(3, [[1, 0], [2, 0]], 0),
(4, [[0, 1], [1, 2], [2, 3]], 3),
(6, [[0, 1], [2, 0], [3, 2], [4, 3], [5, 4]], 1),
(5, [[1, 0], [2, 1], [3, 2], [4, 3]], 0),
(7, [[0, 1], [2, 0], [3, 2], [4, 3], [5, 3], [6, 5]], 1),
]

class ReorderRoutesTestCase(unittest.TestCase):
def test_1(self):
"""should return 3 from input n = 6, connections = [[0,1],[1,3],[2,3],[4,0],[4,5]]"""
connections = [[0, 1], [1, 3], [2, 3], [4, 0], [4, 5]]
n = 6
expected = 3
actual = Solution().min_reorder(n, connections)
self.assertEqual(expected, actual)

def test_2(self):
"""should return 2 from input n = 5, connections = [[1,0],[1,2],[3,2],[3,4]]"""
connections = [[1, 0], [1, 2], [3, 2], [3, 4]]
n = 5
expected = 2
actual = Solution().min_reorder(n, connections)
self.assertEqual(expected, actual)

def test_3(self):
"""should return 0 from input n = 3, connections = [[1,0],[2,0]]"""
connections = [[1, 0], [2, 0]]
n = 3
expected = 0
actual = Solution().min_reorder(n, connections)
class ReorderRoutesTestCase(unittest.TestCase):
@parameterized.expand(REORDER_ROUTES_TEST_CASES)
def test_min_reorder_routes(
self, n: int, connections: List[List[int]], expected: int
):
actual = min_reorder(n, connections)
self.assertEqual(expected, actual)


Expand Down
Loading