2368. Reachable Nodes With Restrictions
Problem Description
You have an undirected tree with n
nodes labeled from 0
to n - 1
, connected by n - 1
edges. The tree structure is given as a 2D array edges
, where each edges[i] = [ai, bi]
represents an edge between nodes ai
and bi
.
Some nodes in the tree are marked as restricted, given in the array restricted
. These restricted nodes cannot be visited.
Your task is to find the maximum number of nodes you can reach starting from node 0
, without visiting any restricted nodes. Node 0
itself is guaranteed to never be restricted.
For example, if you have a tree where node 0
connects to nodes 1
, 2
, and 3
, but node 2
is restricted, you can only reach nodes 0
, 1
, and 3
(assuming nodes 1
and 3
don't lead to other unrestricted nodes). The answer would be 3.
The solution uses depth-first search (DFS) to traverse the tree from node 0
, counting all reachable nodes while avoiding the restricted ones. It builds an adjacency list to represent the tree structure and uses a visited set (initialized with restricted nodes) to track which nodes should not be explored.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem explicitly involves an undirected tree with nodes and edges. A tree is a special type of graph (connected and acyclic).
Is it a tree?
- Yes: The problem states we have an undirected tree with
n
nodes andn-1
edges, which is the exact definition of a tree structure.
DFS
- Conclusion: Since we're dealing with a tree structure, the flowchart leads us directly to DFS (Depth-First Search).
This makes perfect sense for our problem because:
- We need to traverse from node
0
and explore all reachable nodes - We need to count nodes while avoiding restricted ones
- DFS naturally explores all paths from a starting node in a tree
- The recursive nature of DFS makes it easy to count nodes as we traverse
The flowchart confirms that DFS is the appropriate algorithm for this tree traversal problem where we need to explore all reachable nodes from a starting point while respecting certain constraints (restricted nodes).
Intuition
Since we've identified this as a tree traversal problem using DFS, let's think about what we're actually trying to do. We want to count all nodes we can reach from node 0
, but we need to avoid the restricted nodes - they act like barriers that block our path.
Think of it like exploring a network of rooms where some doors are locked (restricted nodes). Starting from room 0
, we want to count how many rooms we can actually visit. If we encounter a locked door, we simply don't go through it, which means we can't reach any rooms that are only accessible through that locked door.
The key insight is that restricted nodes effectively "cut off" portions of the tree. When we hit a restricted node during our traversal, it's like hitting a dead end - we can't go further in that direction. This naturally suggests a DFS approach where we:
- Start from node
0
- Explore each neighbor recursively
- Skip any neighbor that is restricted
- Count each node we successfully visit
To implement this efficiently, we can treat restricted nodes as if they're already visited before we even start our traversal. By adding them to our visited
set initially, we ensure our DFS will never attempt to explore them. This is clever because it unifies our logic - we don't need separate checks for "is this restricted?" and "have I visited this?" - both cases are handled by the same visited
set.
The recursive DFS naturally accumulates the count: each call returns 1
(for the current node) plus the sum of all nodes reachable from its unvisited neighbors. This builds up our total count as the recursion unwinds, giving us the final answer when we return from the initial call to dfs(0)
.
Learn more about Tree, Depth-First Search, Breadth-First Search, Union Find and Graph patterns.
Solution Approach
Let's implement the DFS solution step by step:
Step 1: Build the Graph Representation
First, we need to convert the edge list into an adjacency list for easier traversal. We use a defaultdict(list)
to store the graph:
g = defaultdict(list)
for a, b in edges:
g[a].append(b)
g[b].append(a)
Since the tree is undirected, we add both directions for each edge. This allows us to traverse from any node to its neighbors.
Step 2: Initialize the Visited Set
Here's the clever part - we initialize our vis
set with the restricted nodes:
vis = set(restricted)
By marking restricted nodes as "visited" from the start, we ensure our DFS will never explore them. This elegantly handles the restriction without needing additional conditional checks during traversal.
Step 3: Implement the DFS Function
The recursive DFS function counts reachable nodes:
def dfs(i: int) -> int:
vis.add(i)
return 1 + sum(j not in vis and dfs(j) for j in g[i])
Let's break down what happens in each call:
vis.add(i)
: Mark the current node as visited to avoid cycles1
: Count the current node itselfsum(j not in vis and dfs(j) for j in g[i])
: For each neighborj
:- If
j
is not visited (and not restricted, since restricted nodes are already invis
) - Recursively call
dfs(j)
and add its count - The expression
j not in vis and dfs(j)
uses short-circuit evaluation - ifj
is already visited, it returnsFalse
(which equals0
in the sum)
- If
Step 4: Start the Traversal
Finally, we start the DFS from node 0
:
return dfs(0)
The function will explore all reachable nodes from node 0
, automatically avoiding restricted nodes, and return the total count.
Time Complexity: O(n)
where n
is the number of nodes, as we visit each reachable node exactly once.
Space Complexity: O(n)
for the adjacency list, visited set, and recursion stack in the worst case.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Example:
- Tree with 7 nodes (0-6)
- Edges:
[[0,1], [1,2], [1,3], [3,4], [0,5], [5,6]]
- Restricted nodes:
[2, 4]
The tree looks like this:
0 / \ 1 5 / \ \ 2 3 6 | 4
Step 1: Build the adjacency list
g = { 0: [1, 5], 1: [0, 2, 3], 2: [1], 3: [1, 4], 4: [3], 5: [0, 6], 6: [5] }
Step 2: Initialize visited set with restricted nodes
vis = {2, 4} # Nodes 2 and 4 are "pre-blocked"
Step 3: Execute DFS from node 0
Let's trace through the DFS execution:
-
dfs(0):
- Add 0 to vis:
vis = {2, 4, 0}
- Check neighbors [1, 5]:
- 1 not in vis → call dfs(1)
- 5 not in vis → call dfs(5)
- Returns:
1 + dfs(1) + dfs(5)
- Add 0 to vis:
-
dfs(1):
- Add 1 to vis:
vis = {2, 4, 0, 1}
- Check neighbors [0, 2, 3]:
- 0 in vis → skip (returns 0)
- 2 in vis (restricted) → skip (returns 0)
- 3 not in vis → call dfs(3)
- Returns:
1 + 0 + 0 + dfs(3) = 1 + dfs(3)
- Add 1 to vis:
-
dfs(3):
- Add 3 to vis:
vis = {2, 4, 0, 1, 3}
- Check neighbors [1, 4]:
- 1 in vis → skip (returns 0)
- 4 in vis (restricted) → skip (returns 0)
- Returns:
1 + 0 + 0 = 1
- Add 3 to vis:
-
Back to dfs(1): Returns
1 + 1 = 2
-
dfs(5):
- Add 5 to vis:
vis = {2, 4, 0, 1, 3, 5}
- Check neighbors [0, 6]:
- 0 in vis → skip (returns 0)
- 6 not in vis → call dfs(6)
- Returns:
1 + 0 + dfs(6)
- Add 5 to vis:
-
dfs(6):
- Add 6 to vis:
vis = {2, 4, 0, 1, 3, 5, 6}
- Check neighbors [5]:
- 5 in vis → skip (returns 0)
- Returns:
1 + 0 = 1
- Add 6 to vis:
-
Back to dfs(5): Returns
1 + 1 = 2
-
Back to dfs(0): Returns
1 + 2 + 2 = 5
Final Answer: 5
We successfully reached nodes {0, 1, 3, 5, 6}, which is 5 nodes total. Nodes 2 and 4 were restricted and blocked our traversal, preventing us from exploring beyond them.
Solution Implementation
1from collections import defaultdict
2from typing import List
3
4class Solution:
5 def reachableNodes(
6 self, n: int, edges: List[List[int]], restricted: List[int]
7 ) -> int:
8 """
9 Count the number of reachable nodes from node 0 in an undirected graph,
10 excluding restricted nodes.
11
12 Args:
13 n: Total number of nodes in the graph (0 to n-1)
14 edges: List of edges where each edge is [node_a, node_b]
15 restricted: List of restricted nodes that cannot be visited
16
17 Returns:
18 Number of nodes reachable from node 0
19 """
20
21 def dfs(current_node: int) -> int:
22 """
23 Depth-first search to count reachable nodes.
24
25 Args:
26 current_node: The current node being visited
27
28 Returns:
29 Count of nodes reachable from current_node (including itself)
30 """
31 # Mark current node as visited
32 visited.add(current_node)
33
34 # Count current node (1) plus all unvisited neighbors recursively
35 reachable_count = 1
36 for neighbor in adjacency_list[current_node]:
37 if neighbor not in visited:
38 reachable_count += dfs(neighbor)
39
40 return reachable_count
41
42 # Build adjacency list representation of the undirected graph
43 adjacency_list = defaultdict(list)
44 for node_a, node_b in edges:
45 adjacency_list[node_a].append(node_b)
46 adjacency_list[node_b].append(node_a)
47
48 # Initialize visited set with restricted nodes to prevent visiting them
49 visited = set(restricted)
50
51 # Start DFS from node 0 and return the count of reachable nodes
52 return dfs(0)
53
1class Solution {
2 // Adjacency list to represent the graph
3 private List<Integer>[] adjacencyList;
4 // Boolean array to track visited nodes
5 private boolean[] visited;
6
7 public int reachableNodes(int n, int[][] edges, int[] restricted) {
8 // Initialize the adjacency list with n empty lists
9 adjacencyList = new List[n];
10 visited = new boolean[n];
11 Arrays.setAll(adjacencyList, index -> new ArrayList<>());
12
13 // Build the undirected graph by adding edges in both directions
14 for (int[] edge : edges) {
15 int nodeA = edge[0];
16 int nodeB = edge[1];
17 adjacencyList[nodeA].add(nodeB);
18 adjacencyList[nodeB].add(nodeA);
19 }
20
21 // Mark all restricted nodes as visited to prevent traversal
22 for (int restrictedNode : restricted) {
23 visited[restrictedNode] = true;
24 }
25
26 // Start DFS from node 0 and return the count of reachable nodes
27 return dfs(0);
28 }
29
30 private int dfs(int currentNode) {
31 // Mark the current node as visited
32 visited[currentNode] = true;
33 // Count the current node
34 int nodeCount = 1;
35
36 // Explore all adjacent nodes
37 for (int neighbor : adjacencyList[currentNode]) {
38 // If the neighbor hasn't been visited, recursively explore it
39 if (!visited[neighbor]) {
40 nodeCount += dfs(neighbor);
41 }
42 }
43
44 return nodeCount;
45 }
46}
47
1class Solution {
2public:
3 int reachableNodes(int n, vector<vector<int>>& edges, vector<int>& restricted) {
4 // Build adjacency list representation of the graph
5 vector<vector<int>> adjacencyList(n);
6
7 // Mark visited nodes (initially marking restricted nodes as visited)
8 vector<bool> isVisited(n, false);
9
10 // Construct the undirected graph
11 for (const auto& edge : edges) {
12 int nodeA = edge[0];
13 int nodeB = edge[1];
14 adjacencyList[nodeA].push_back(nodeB);
15 adjacencyList[nodeB].push_back(nodeA);
16 }
17
18 // Mark all restricted nodes as visited to prevent traversal
19 for (int restrictedNode : restricted) {
20 isVisited[restrictedNode] = true;
21 }
22
23 // Define DFS function to count reachable nodes
24 function<int(int)> depthFirstSearch = [&](int currentNode) -> int {
25 // Mark current node as visited
26 isVisited[currentNode] = true;
27
28 // Count current node
29 int nodeCount = 1;
30
31 // Explore all unvisited neighbors
32 for (int neighbor : adjacencyList[currentNode]) {
33 if (!isVisited[neighbor]) {
34 nodeCount += depthFirstSearch(neighbor);
35 }
36 }
37
38 return nodeCount;
39 };
40
41 // Start DFS from node 0 and return the count of reachable nodes
42 return depthFirstSearch(0);
43 }
44};
45
1/**
2 * Counts the number of reachable nodes from node 0 in an undirected tree,
3 * excluding restricted nodes.
4 *
5 * @param n - The total number of nodes (0 to n-1)
6 * @param edges - Array of edges where each edge is [node1, node2]
7 * @param restricted - Array of restricted node indices that cannot be visited
8 * @returns The count of nodes reachable from node 0
9 */
10function reachableNodes(n: number, edges: number[][], restricted: number[]): number {
11 // Track visited nodes to avoid revisiting
12 const visited: boolean[] = Array(n).fill(false);
13
14 // Build adjacency list representation of the graph
15 const adjacencyList: number[][] = Array.from({ length: n }, () => []);
16
17 // Populate the adjacency list with bidirectional edges
18 for (const [nodeA, nodeB] of edges) {
19 adjacencyList[nodeA].push(nodeB);
20 adjacencyList[nodeB].push(nodeA);
21 }
22
23 // Mark all restricted nodes as visited to prevent traversal
24 for (const restrictedNode of restricted) {
25 visited[restrictedNode] = true;
26 }
27
28 /**
29 * Performs depth-first search to count reachable nodes
30 *
31 * @param currentNode - The current node being explored
32 * @returns The count of nodes in this connected component
33 */
34 const dfs = (currentNode: number): number => {
35 // Mark current node as visited
36 visited[currentNode] = true;
37
38 // Start count with current node
39 let nodeCount = 1;
40
41 // Explore all neighbors
42 for (const neighbor of adjacencyList[currentNode]) {
43 if (!visited[neighbor]) {
44 // Recursively count nodes from unvisited neighbors
45 nodeCount += dfs(neighbor);
46 }
47 }
48
49 return nodeCount;
50 };
51
52 // Start DFS from node 0 and return the total count
53 return dfs(0);
54}
55
Time and Space Complexity
Time Complexity: O(n)
The algorithm performs a depth-first search (DFS) starting from node 0. In the worst case, it visits all reachable nodes exactly once. Since each node is visited at most once (tracked by the vis
set), and for each visited node we iterate through its adjacent edges, the total time complexity is O(V + E)
where V
is the number of vertices and E
is the number of edges. Given that this is a tree structure (as implied by having n
nodes and n-1
edges), we have E = n - 1
. Therefore, the time complexity simplifies to O(n + (n-1)) = O(n)
.
Space Complexity: O(n)
The space complexity consists of several components:
- The adjacency list
g
stores all edges, which requiresO(n)
space since there aren-1
edges in a tree, and each edge is stored twice (bidirectional). - The
vis
set can contain up ton
nodes in the worst case, requiringO(n)
space. - The recursive DFS call stack can go up to depth
n
in the worst case (when the tree is a linear chain), requiringO(n)
space.
Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Forgetting to Handle Node 0 in Restricted Set
While the problem statement guarantees that node 0 is never restricted, some developers might accidentally add error-checking code that could break the solution:
Incorrect Approach:
def reachableNodes(self, n, edges, restricted):
# Wrong: Trying to "handle" node 0 being restricted
if 0 in restricted:
return 0 # This will never happen per problem constraints
# ... rest of the code
Why it's problematic: Adding unnecessary checks can introduce bugs and confusion. Trust the problem constraints.
Pitfall 2: Using Global Variables Incorrectly
When using the compact DFS with closure, forgetting that vis
needs to be accessible can cause issues:
Incorrect Approach:
def reachableNodes(self, n, edges, restricted):
g = defaultdict(list)
for a, b in edges:
g[a].append(b)
g[b].append(a)
def dfs(i):
vis = set(restricted) # Wrong: Creating new set in each call
vis.add(i)
return 1 + sum(j not in vis and dfs(j) for j in g[i])
return dfs(0)
Correct Approach:
def reachableNodes(self, n, edges, restricted):
g = defaultdict(list)
for a, b in edges:
g[a].append(b)
g[b].append(a)
vis = set(restricted) # Correct: vis is in outer scope
def dfs(i):
vis.add(i)
return 1 + sum(j not in vis and dfs(j) for j in g[i])
return dfs(0)
Pitfall 3: Short-Circuit Evaluation Order
The compact expression j not in vis and dfs(j)
relies on short-circuit evaluation. Reversing the order breaks the logic:
Incorrect Approach:
def dfs(i):
vis.add(i)
return 1 + sum(dfs(j) and j not in vis for j in g[i]) # Wrong order!
Why it fails: This calls dfs(j)
first, which adds j
to vis
, making j not in vis
always False. This leads to infinite recursion or incorrect results.
Pitfall 4: Modifying Restricted List
Attempting to modify the input restricted
list directly instead of creating a new set:
Incorrect Approach:
def reachableNodes(self, n, edges, restricted):
g = defaultdict(list)
for a, b in edges:
g[a].append(b)
g[b].append(a)
restricted.append(0) # Wrong: Modifying input
vis = restricted # Wrong: vis is a list, not a set
def dfs(i):
if i not in vis: # O(n) lookup in list!
vis.append(i)
# ... rest of logic
Problems:
- Modifies the input array (side effects)
- List lookups are O(n) instead of O(1)
- Can cause duplicate entries in the list
Correct Approach: Always create a new set from the restricted list:
vis = set(restricted) # Creates a new set, O(1) lookups
Pitfall 5: Handling Empty Graphs or Edge Cases
Not considering edge cases like when there are no edges or only restricted nodes:
Incomplete Approach:
def reachableNodes(self, n, edges, restricted):
if not edges: # What if n=1 and no edges?
return 0 # Wrong: Should return 1 (node 0 itself)
# ... rest of code
Correct Understanding:
- If
n=1
andedges=[]
, the answer is 1 (just node 0) - If all nodes except 0 are restricted, the answer is 1
- The base case is handled naturally by the DFS returning 1 for the starting node
The beauty of the provided solution is that it handles all these edge cases naturally without special conditionals.
Which algorithm should you use to find a node that is close to the root of the tree?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Want a Structured Path to Master System Design Too? Don’t Miss This!