Skip to content

Commit 7f2ad34

Browse files
authored
Merge pull request #135 from BrianLusina/feat/algorithms-graphs-reorder-routes
refactor(algorithms, graphs): reorder routes
2 parents d9861d8 + 2e825f6 commit 7f2ad34

14 files changed

+150
-84
lines changed

algorithms/graphs/reorder_routes/README.md

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@ It's guaranteed that each city can reach city 0 after reorder.
1515

1616
Example 1
1717

18-
![](reorder_routes_example_1.png)
18+
![](images/examples/reorder_routes_example_1.png)
1919

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

2424
Example 2
2525

26-
![](reorder_routes_example_2.png)
26+
![](images/examples/reorder_routes_example_2.png)
2727

2828
Input: n = 5, connections = [[1,0],[1,2],[3,2],[3,4]]
2929
Output: 2
@@ -33,6 +33,57 @@ Example 3
3333
Input: n = 3, connections = [[1,0],[2,0]]
3434
Output: 0
3535

36+
## Solution
37+
38+
This algorithm works because representing the network as a graph with directional information allows us to easily
39+
identify which roads are misoriented. By performing a DFS from the capital (city 0), we traverse the entire network
40+
exactly once. Every time we encounter a road directed away from city 0, we know it needs to be reversed. This approach
41+
takes advantage of the network’s tree structure to ensure we only count the minimum number of necessary reversals.
42+
43+
Now, let’s look at the solution steps below:
44+
45+
1. Build an adjacency list using a variable graph to store the roads and their directional flags.
46+
- For each connection [ai, bi] in connections:
47+
- Append (bi, 1) to graph[ai], where the flag 1 indicates that the road goes from ai to bi and might need to be
48+
reversed.
49+
- Append (ai, 0) to graph[bi], where the flag 0 represents the reverse edge, which is correctly oriented for the
50+
DFS.
51+
2. Create a set, visited, to track which cities have been processed, preventing repeated visits.
52+
3. Call dfs(0, graph, visited) to start the DFS from the capital (city 0) and store the result in a variable result.
53+
4. Return result as the final answer is the minimum number of road reorientations needed.
54+
55+
**Define the DFS function**
56+
57+
1. Start the DFS from city 0
58+
- Add the current city to the visited set.
59+
- Initializes a variable, reversals, to count the number of road reversals needed for the subtree rooted at that city.
60+
- For each neighbor and its associated flag (represented by need_reverse) in graph[city]:
61+
- If the neighbor hasn’t been visited, add need_reverse to result (As need_reverse equals 1 if the road needs reversal).
62+
- Recursively call dfs(neighbor, graph, visited) to continue the traversal.
63+
- Returns reversals.
64+
65+
Let’s look at the following illustration to get a better understanding of the solution:
66+
67+
![Solution 1](./images/solutions/reorder_routes_solution_1.png)
68+
![Solution 2](./images/solutions/reorder_routes_solution_2.png)
69+
![Solution 3](./images/solutions/reorder_routes_solution_3.png)
70+
![Solution 4](./images/solutions/reorder_routes_solution_4.png)
71+
![Solution 5](./images/solutions/reorder_routes_solution_5.png)
72+
![Solution 6](./images/solutions/reorder_routes_solution_6.png)
73+
![Solution 7](./images/solutions/reorder_routes_solution_7.png)
74+
![Solution 8](./images/solutions/reorder_routes_solution_8.png)
75+
![Solution 9](./images/solutions/reorder_routes_solution_9.png)
76+
77+
### Time Complexity
78+
79+
The time complexity of the solution is O(n), where n is the number of cities because every node and its corresponding
80+
edges are visited exactly once during the DFS.
81+
82+
### Space Complexity
83+
84+
The solution’s space complexity is O(n) due to the storage needed for the graph (adjacency list), the visited set, and
85+
the recursion call stack.
86+
3687
## Related Topics
3788

3889
- Depth First Search

algorithms/graphs/reorder_routes/__init__.py

Lines changed: 79 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,65 +2,85 @@
22
from collections import defaultdict
33

44

5-
class Solution:
5+
def min_reorder(n: int, connections: List[List[int]]) -> int:
6+
"""Reorders the edges of the directed graph represented as an adjacency list in connections such that each vertex
7+
is connected and has a path to the first vertex marked with 0. n represents the number of vertices.
8+
9+
Complexity;
10+
n is the number of vertices/nodes
11+
12+
Time Complexity: O(n)
13+
O(n) to initialize the adjacency list
14+
The dfs function visits each node once, which takes O(n) time in total. Because we have undirected edges,
15+
each edge can only be iterated twice (by nodes at the end), resulting in O(e) operations total while
16+
visiting all nodes, where e is the number of edges. Because the given graph is a tree, there are n−1
17+
undirected edges, so O(n+e)=O(n).
18+
19+
Space Complexity: O(n)
20+
Building the adjacency list takes O(n) space.
21+
The recursion call stack used by dfs can have no more than n elements in the worst-case scenario.
22+
It would take up O(n) space in that case.
23+
24+
Args:
25+
n(int): number of vertices or in this case, number of cities
26+
connections(list): adjacency matrix for a directed graph or in this case, representation of cities
27+
Returns:
28+
int: minimum number of edges to re-arrange to ensure that each vertex is directly or indirectly connected to
29+
the initial vertex
30+
"""
31+
32+
# Adjacency list that contains list of pairs of nodes such that adj[node] contains all the neighbours of node in the
33+
# form of [neighbour, sign] where neighbour is the neighbouring node and sign is the direction of the edge. If the
34+
# sign is 0, it's an 'artificial' edge, meaning it was added by the algorithm in order to get to this vertex, and 1
35+
# denotes that it's an 'original' edge, meaning that it's the original edge and no need to re-order that connection
36+
adj: Dict[int, List[List[int]]] = defaultdict(lambda: [])
37+
638
# keep track of number of reorders made
739
reorder_count = 0
840

9-
def min_reorder(self, n: int, connections: List[List[int]]) -> int:
10-
"""Reorders the edges of the directed graph represented as an adjacency list in connections such that each vertex
11-
is connected and has a path to the first vertex marked with 0. n represents the number of vertices.
12-
13-
Complexity;
14-
n is the number of vertices/nodes
15-
16-
Time Complexity: O(n)
17-
O(n) to initialize the adjacency list
18-
The dfs function visits each node once, which takes O(n) time in total. Because we have undirected edges,
19-
each edge can only be iterated twice (by nodes at the end), resulting in O(e) operations total while
20-
visiting all nodes, where e is the number of edges. Because the given graph is a tree, there are n−1
21-
undirected edges, so O(n+e)=O(n).
22-
23-
Space Complexity: O(n)
24-
Building the adjacency list takes O(n) space.
25-
The recursion call stack used by dfs can have no more than n elements in the worst-case scenario.
26-
It would take up O(n) space in that case.
27-
28-
Args:
29-
n(int): number of vertices or in this case, number of cities
30-
connections(list): adjacency matrix for a directed graph or in this case, representation of cities
31-
Returns:
32-
int: minimum number of edges to re-arrange to ensure that each vertex is directly or indirectly connected to
33-
the initial vertex
34-
"""
35-
36-
# Adjacency list that contains list of pairs of nodes such that adj[node] contains all the neighbours of node in the
37-
# form of [neighbour, sign] where neighbour is the neighbouring node and sign is the direction of the edge. If the
38-
# sign is 0, it's an 'artificial' edge, meaning it was added by the algorithm in order to get to this vertex, and 1
39-
# denotes that it's an 'original' edge, meaning that it's the original edge and no need to re-order that connection
40-
adj: Dict[int, List[List[int]]] = defaultdict(lambda: [])
41-
42-
def dfs(node: int, parent: int, adjacency: Dict[int, List[List[int]]]):
43-
if node not in adjacency:
44-
return
45-
46-
# iterate over all children of node(nodes that share an edge)
47-
# for every child, sign, check if child is equal to parent. if child is equal to parent, we will not visit it
48-
# again
49-
# if child is not equal to parent, we perform count+=sign and recursively call the dfs with node = child and
50-
# parent = node
51-
for adjacent_node in adjacency.get(node):
52-
child = adjacent_node[0]
53-
sign = adjacent_node[1]
54-
55-
if child != parent:
56-
self.reorder_count += sign
57-
dfs(child, node, adjacency)
58-
59-
for connection in connections:
60-
adj[connection[0]].append([connection[1], 1])
61-
adj[connection[1]].append([connection[0], 0])
62-
63-
# we start with node, parent and the adjacency list as 0, and -1 and adj
64-
dfs(0, -1, adj)
65-
66-
return self.reorder_count
41+
def dfs(node: int, parent: int, adjacency: Dict[int, List[List[int]]]):
42+
nonlocal reorder_count
43+
if node not in adjacency:
44+
return
45+
46+
# iterate over all children of node(nodes that share an edge)
47+
# for every child, sign, check if child is equal to parent. if child is equal to parent, we will not visit it
48+
# again
49+
# if child is not equal to parent, we perform count+=sign and recursively call the dfs with node = child and
50+
# parent = node
51+
for adjacent_node in adjacency.get(node):
52+
child = adjacent_node[0]
53+
sign = adjacent_node[1]
54+
55+
if child != parent:
56+
reorder_count += sign
57+
dfs(child, node, adjacency)
58+
59+
for connection in connections:
60+
adj[connection[0]].append([connection[1], 1])
61+
adj[connection[1]].append([connection[0], 0])
62+
63+
# we start with node, parent and the adjacency list as 0, and -1 and adj
64+
dfs(0, -1, adj)
65+
66+
return reorder_count
67+
68+
69+
def min_reorder_2(n: int, connections: List[List[int]]) -> int:
70+
def dfs(city, graph, visited):
71+
visited.add(city)
72+
reversals = 0
73+
for neighbor, need_reverse in graph[city]:
74+
if neighbor not in visited:
75+
reversals += need_reverse
76+
reversals += dfs(neighbor, graph, visited)
77+
return reversals
78+
79+
graph = defaultdict(list)
80+
for ai, bi in connections:
81+
graph[ai].append((bi, 1))
82+
graph[bi].append((ai, 0))
83+
84+
visited = set()
85+
result = dfs(0, graph, visited)
86+
return result

algorithms/graphs/reorder_routes/reorder_routes_example_1.png renamed to algorithms/graphs/reorder_routes/images/examples/reorder_routes_example_1.png

File renamed without changes.

algorithms/graphs/reorder_routes/reorder_routes_example_2.png renamed to algorithms/graphs/reorder_routes/images/examples/reorder_routes_example_2.png

File renamed without changes.
52 KB
Loading
101 KB
Loading
131 KB
Loading
119 KB
Loading
107 KB
Loading
133 KB
Loading

0 commit comments

Comments
 (0)