Skip to content

Commit b50ebc0

Browse files
committed
feat(graphs, bfs): closest node in tree along path
1 parent d851df5 commit b50ebc0

File tree

9 files changed

+167
-8
lines changed

9 files changed

+167
-8
lines changed

datastructures/graphs/graph.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
from pprint import PrettyPrinter
44
from typing import List, Set, Union, Generic, TypeVar
55
from datastructures.stacks import Stack
6-
from .vertex import Vertex
7-
from .edge import Edge
6+
from datastructures.graphs.vertex import Vertex
7+
from datastructures.graphs.edge import Edge
88

99
T = TypeVar("T")
1010

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Undirected Graph
2+
3+
## Closest Node to Path in Tree
4+
5+
You are given a positive integer, n, representing the number of nodes in a tree, numbered from `0 to n−1`. You are also
6+
given a 2D integer array edges of length `n−1`, where `edges[i] = [ui, vi]` indicates that there is a bidirectional edge
7+
connecting nodes `ui` and `vi`.
8+
9+
You are also given a 2D integer array query of length `m`, where `query[i]=[starti ,endi ,nodei]`. For each query i,
10+
find the node on the path between `starti` and `endi` that is closest to `nodei` in terms of the number of edges.
11+
12+
Return an integer array where the value at index `i` corresponds to the answer for the `ith` query.
13+
14+
> Note: If there are multiple such nodes at the same minimum distance, return the one with the smallest index.
15+
16+
Constraints
17+
- `1 ≤ n ≤ 1000`
18+
- `edges.length == n-1`
19+
- `edges[i].length == 2`
20+
- `0 ≤ ui, vi ≤ n-1`
21+
- `ui != vi`
22+
- `1 ≤ query.length ≤ 1000`
23+
- `query[i].length == 3`
24+
- `0 ≤ starti, endi, nodei ≤ n-1`
25+
- `the graph is a tree`
26+
27+
### Examples
28+
29+
![Example 1](./images/examples/closest_node_to_path_in_tree_example_1.png)
30+
![Example 2](./images/examples/closest_node_to_path_in_tree_example_2.png)
31+
![Example 3](./images/examples/closest_node_to_path_in_tree_example_3.png)
Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
from typing import List
1+
from datastructures.graphs.undirected.undirected_graph import UndirectedGraph
22

3-
from datastructures.graphs import Edge, Graph
43

5-
6-
class UnDirectedGraph(Graph):
7-
def __init__(self, edge_list: List[Edge]):
8-
super(UnDirectedGraph, self).__init__(edge_list)
4+
__all__ = ["UndirectedGraph"]
92.6 KB
Loading
102 KB
Loading
139 KB
Loading
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import unittest
2+
from typing import List
3+
from parameterized import parameterized
4+
from datastructures.graphs.undirected.utils import closest_node
5+
6+
7+
class ClosestNodeToPathInTreeTestCase(unittest.TestCase):
8+
9+
@parameterized.expand([
10+
(3, [[0,1],[1,2]], [[0,2,1]], [1]),
11+
(4, [[0,1],[1,2],[1,3]], [[2,3,0]], [1]),
12+
(6, [[0,1],[0,2],[0,3],[0,4],[0,5]], [[1,5,2],[2,3,4]], [0,0]),
13+
(7, [[0,1],[0,2],[0,3],[1,4],[2,5],[2,6]], [[5,3,4],[5,3,6]], [0,2]),
14+
(3, [[0,1],[1,2]], [[0,1,2]], [1]),
15+
(3, [[0,1],[1,2]], [[0,0,0]], [0]),
16+
])
17+
def test_closest_node(self, n: int, edges: List[List[int]], query: List[List[int]], expected: int):
18+
actual = closest_node(n, edges, query)
19+
self.assertEqual(expected, actual)
20+
21+
22+
if __name__ == '__main__':
23+
unittest.main()
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from typing import List, Set
2+
3+
from datastructures.graphs import Edge, Graph, Vertex
4+
5+
6+
class UndirectedGraph(Graph):
7+
8+
def __init__(self, edge_list: List[Edge]):
9+
super(UndirectedGraph, self).__init__(edge_list)
10+
11+
def bfs_from_root_to_target(self, root: Vertex, target: Vertex) -> Set[Vertex]:
12+
pass
13+
14+
def bfs_from_node(self, source: Vertex) -> Set[Vertex]:
15+
pass
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from typing import List, DefaultDict, Dict, Optional
2+
from collections import defaultdict, deque
3+
4+
5+
def closest_node(n: int, edges: List[List[int]], query: List[List[int]]) -> List[int]:
6+
# build and adjacency list
7+
adj_list: DefaultDict[int, List[int]] = defaultdict(list)
8+
for u, v in edges:
9+
adj_list[u].append(v)
10+
adj_list[v].append(u)
11+
12+
def bfs_distance(start: int) -> List[int]:
13+
"""
14+
Compute the shortest distance from start node to all other nodes using BFS.
15+
Returns a list where distances[i] is the distance from start to node i.
16+
"""
17+
# -1 means not visited
18+
distances = [-1] * n
19+
distances[start] = 0
20+
queue = deque([start])
21+
22+
while queue:
23+
node = queue.popleft()
24+
for neighbor in adj_list[node]:
25+
if distances[neighbor] == -1: # Not visited
26+
distances[neighbor] = distances[node] + 1
27+
queue.append(neighbor)
28+
29+
return distances
30+
31+
def find_path(start: int, end: int) -> List[int]:
32+
"""
33+
Find the unique path from start to end in the tree using BFS.
34+
Returns the list of nodes on the path (including start and end).
35+
"""
36+
if start == end:
37+
return [start]
38+
39+
# BFS to find path, keeping track of parent pointers
40+
parent: Dict[int, Optional[int]] = {start: None}
41+
queue = deque([start])
42+
43+
while queue:
44+
node = queue.popleft()
45+
if node == end:
46+
break
47+
48+
for neighbor in adj_list[node]:
49+
# not visited
50+
if neighbor not in parent:
51+
parent[neighbor] = node
52+
queue.append(neighbor)
53+
54+
# Reconstruct path from end to start using pointers
55+
path = []
56+
current = end
57+
# Add safety check: limit iterations to avoid infinite loops
58+
# In a tree with n nodes, path length can't exceed n
59+
for _ in range(n):
60+
if current not in parent:
61+
# This shouldn't happen in a valid connected tree
62+
break
63+
path.append(current)
64+
if current == start:
65+
# We've reached the start, we're done
66+
break
67+
current = parent[current]
68+
69+
# reverse to get the path start to end
70+
return path[::-1]
71+
72+
result = []
73+
for start, end, target in query:
74+
# find the path from start to end
75+
found_path = find_path(start, end)
76+
77+
# compute distances from target node to all nodes
78+
distances_from_target = bfs_distance(target)
79+
80+
# Find the node on the path with minimum distance to target
81+
# If there's a tie, we want the one with the smallest index
82+
min_distance = float('inf')
83+
closest = -1
84+
85+
for node in found_path:
86+
dist = distances_from_target[node]
87+
# Update if we found a closer node, or same distance but smaller index
88+
if dist < min_distance or (dist == min_distance and node < closest):
89+
min_distance = dist
90+
closest = node
91+
92+
result.append(closest)
93+
94+
return result

0 commit comments

Comments
 (0)