1557. Minimum Number of Vertices to Reach All Nodes
Problem Description
You are given a directed acyclic graph (DAG) with n
vertices labeled from 0
to n-1
. The graph is represented by an array edges
where each element edges[i] = [from_i, to_i]
indicates a directed edge from vertex from_i
to vertex to_i
.
Your task is to find the smallest set of vertices such that starting from these vertices, you can reach every vertex in the graph. The problem guarantees that there is exactly one unique solution.
The key insight is that in a directed graph, a vertex needs to be in the starting set if and only if it has no incoming edges. This is because:
- If a vertex has incoming edges, it can be reached from other vertices, so it doesn't need to be a starting point
- If a vertex has no incoming edges (in-degree of 0), there's no way to reach it from other vertices, so it must be included in the starting set
The solution counts the incoming edges for each vertex using a Counter
. Any vertex i
that has zero incoming edges (cnt[i] == 0
) must be part of the minimum starting set. These vertices with no predecessors form the smallest set from which all other vertices can be reached through the directed edges.
For example, if you have edges [[0,1], [0,2], [2,3], [3,4]]
, vertices 1, 2, 3, and 4 all have incoming edges, but vertex 0 has none. Therefore, the answer would be [0]
since starting from vertex 0, you can reach all other vertices in the graph.
Intuition
To understand why we need to find vertices with no incoming edges, let's think about the problem step by step.
First, consider what it means for a vertex to be reachable. In a directed graph, vertex B
is reachable from vertex A
if there's a path of directed edges from A
to B
. Now, if we want to reach all vertices in the graph, we need to carefully choose our starting points.
Think about it this way: if a vertex has an incoming edge, it means some other vertex points to it. This tells us that we can reach this vertex from somewhere else in the graph. So, we don't necessarily need to start from this vertex - we can reach it by starting from its predecessor.
On the other hand, what about vertices with no incoming edges? These vertices are special because there's no way to reach them from any other vertex in the graph. They are "source" vertices that can only serve as starting points, never as destinations from other vertices.
Here's the key realization: since the graph is acyclic (no cycles), if we don't include a vertex with no incoming edges in our starting set, we can never reach it at all! There's no path leading to it from any other vertex.
Therefore, the minimum set of starting vertices must include all vertices with in-degree 0 (no incoming edges). Moreover, this set is also sufficient because:
- Every vertex with incoming edges can be reached from some vertex with fewer incoming edges
- Following this chain backwards in the DAG, we eventually reach a vertex with no incoming edges
- Since we include all such vertices in our starting set, every vertex in the graph becomes reachable
This leads us to the elegant solution: count the incoming edges for each vertex, and return all vertices that have zero incoming edges. These vertices form the unique smallest set from which all nodes are reachable.
Learn more about Graph patterns.
Solution Approach
The implementation is remarkably concise and leverages Python's built-in data structures to efficiently solve the problem.
Step 1: Count Incoming Edges
The solution uses Python's Counter
from the collections module to count the incoming edges for each vertex:
cnt = Counter(t for _, t in edges)
This line deserves careful examination:
edges
contains pairs[from, to]
representing directed edges- The expression
(t for _, t in edges)
creates a generator that extracts only the destination vertices (to
) from each edge - The underscore
_
is used as a placeholder for the source vertex since we don't need it Counter
then counts how many times each destination vertex appears, giving us the in-degree of each vertex
For example, if edges = [[0,1], [0,2], [2,1]]
:
- Vertex 1 appears as a destination twice (in-degree = 2)
- Vertex 2 appears as a destination once (in-degree = 1)
- Vertex 0 never appears as a destination (in-degree = 0)
- The Counter would be:
{1: 2, 2: 1}
Step 2: Identify Vertices with Zero In-degree
return [i for i in range(n) if cnt[i] == 0]
This list comprehension:
- Iterates through all vertices from
0
ton-1
- Checks if each vertex
i
has an in-degree of 0 usingcnt[i] == 0
- Note that if vertex
i
never appeared as a destination in the edges,cnt[i]
returns 0 by default (Counter's behavior for missing keys) - Collects all vertices with zero incoming edges into the result list
Time and Space Complexity:
- Time Complexity:
O(E + V)
where E is the number of edges and V is the number of vertices. We iterate through all edges once to build the counter, then through all vertices once to find those with zero in-degree. - Space Complexity:
O(V)
for storing the counter, which in the worst case could have entries for all vertices.
The beauty of this solution lies in its simplicity - by identifying that vertices with no incoming edges must be in the minimum starting set, we reduce the problem to a simple counting exercise.
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 concrete example to see how the solution works.
Example Graph:
- n = 6 vertices (labeled 0 to 5)
- edges = [[0,1], [0,3], [2,3], [2,4], [3,4], [3,5]]
Step 1: Visualize the Graph
0 β 1 β 3 β 4 β β 2 β 5
Step 2: Count Incoming Edges
We iterate through each edge and count how many times each vertex appears as a destination:
- Edge [0,1]: vertex 1 is a destination
- Edge [0,3]: vertex 3 is a destination
- Edge [2,3]: vertex 3 is a destination (now count = 2)
- Edge [2,4]: vertex 4 is a destination
- Edge [3,4]: vertex 4 is a destination (now count = 2)
- Edge [3,5]: vertex 5 is a destination
Our counter becomes: {1: 1, 3: 2, 4: 2, 5: 1}
Step 3: Identify Vertices with Zero In-degree
Now we check each vertex from 0 to 5:
- Vertex 0: cnt[0] = 0 (not in counter, defaults to 0) β Include
- Vertex 1: cnt[1] = 1 (has incoming edge from 0) β Skip
- Vertex 2: cnt[2] = 0 (not in counter, no incoming edges) β Include
- Vertex 3: cnt[3] = 2 (has incoming edges from 0 and 2) β Skip
- Vertex 4: cnt[4] = 2 (has incoming edges from 2 and 3) β Skip
- Vertex 5: cnt[5] = 1 (has incoming edge from 3) β Skip
Result: [0, 2]
Verification: Starting from vertices 0 and 2, we can indeed reach all vertices:
- From 0: we can reach 1 and 3, and from 3 we can reach 4 and 5
- From 2: we can reach 3 and 4, and from 3 we can reach 5
This is the minimum set because if we excluded either 0 or 2, we wouldn't be able to reach all vertices (vertex 0 has no way to be reached except as a starting point, same for vertex 2).
Solution Implementation
1from typing import List
2from collections import Counter
3
4class Solution:
5 def findSmallestSetOfVertices(self, n: int, edges: List[List[int]]) -> List[int]:
6 # Count the in-degree for each vertex (how many edges point to each vertex)
7 # We only count the destination vertices from edges
8 in_degree_count = Counter(destination for source, destination in edges)
9
10 # Find all vertices with in-degree of 0
11 # These vertices have no incoming edges, so they must be included in the result
12 # to ensure all vertices are reachable
13 result = [vertex for vertex in range(n) if in_degree_count[vertex] == 0]
14
15 return result
16
1class Solution {
2 /**
3 * Finds the smallest set of vertices from which all nodes in a directed graph are reachable.
4 *
5 * The key insight is that vertices with no incoming edges must be included in the result,
6 * as they cannot be reached from any other vertex.
7 *
8 * @param n The number of vertices in the graph (0 to n-1)
9 * @param edges List of directed edges, where each edge is [from, to]
10 * @return List of vertices forming the smallest set from which all nodes are reachable
11 */
12 public List<Integer> findSmallestSetOfVertices(int n, List<List<Integer>> edges) {
13 // Count incoming edges for each vertex
14 int[] inDegreeCount = new int[n];
15
16 // Iterate through all edges and increment the in-degree count for destination vertices
17 for (List<Integer> edge : edges) {
18 int destinationVertex = edge.get(1);
19 inDegreeCount[destinationVertex]++;
20 }
21
22 // Result list to store vertices with zero in-degree
23 List<Integer> result = new ArrayList<>();
24
25 // Find all vertices with no incoming edges (in-degree = 0)
26 // These vertices must be in the minimum set as they cannot be reached from other vertices
27 for (int vertex = 0; vertex < n; vertex++) {
28 if (inDegreeCount[vertex] == 0) {
29 result.add(vertex);
30 }
31 }
32
33 return result;
34 }
35}
36
1class Solution {
2public:
3 vector<int> findSmallestSetOfVertices(int n, vector<vector<int>>& edges) {
4 // Create an array to count incoming edges for each vertex
5 // inDegree[i] represents the number of edges pointing to vertex i
6 vector<int> inDegree(n);
7
8 // Count the incoming edges for each vertex
9 // For each edge from source to destination, increment the in-degree of destination
10 for (const auto& edge : edges) {
11 ++inDegree[edge[1]];
12 }
13
14 // Store the result - vertices with no incoming edges
15 vector<int> result;
16
17 // Find all vertices with zero in-degree
18 // These vertices cannot be reached from any other vertex
19 // Therefore, they must be included in the minimum set
20 for (int vertex = 0; vertex < n; ++vertex) {
21 if (inDegree[vertex] == 0) {
22 result.push_back(vertex);
23 }
24 }
25
26 return result;
27 }
28};
29
1/**
2 * Finds the smallest set of vertices from which all nodes in the graph are reachable.
3 * In a directed acyclic graph (DAG), vertices with no incoming edges must be included
4 * in the result since they cannot be reached from any other vertex.
5 *
6 * @param n - The number of vertices in the graph (labeled from 0 to n-1)
7 * @param edges - Array of directed edges where each edge is [source, target]
8 * @returns Array of vertices with no incoming edges (in-degree of 0)
9 */
10function findSmallestSetOfVertices(n: number, edges: number[][]): number[] {
11 // Initialize array to count incoming edges (in-degree) for each vertex
12 const inDegreeCount: number[] = new Array(n).fill(0);
13
14 // Count the incoming edges for each vertex
15 // We only need to track the target vertices of each edge
16 for (const [source, target] of edges) {
17 inDegreeCount[target]++;
18 }
19
20 // Collect all vertices with no incoming edges
21 // These vertices must be in the minimum set since they can't be reached from others
22 const result: number[] = [];
23 for (let vertex = 0; vertex < n; vertex++) {
24 if (inDegreeCount[vertex] === 0) {
25 result.push(vertex);
26 }
27 }
28
29 return result;
30}
31
Time and Space Complexity
Time Complexity: O(E + N)
where E
is the number of edges and N
is the number of vertices.
- Creating the Counter object requires iterating through all edges once to extract target vertices:
O(E)
- The list comprehension iterates through all vertices from 0 to n-1 to check which ones have zero incoming edges:
O(N)
- Dictionary lookup for
cnt[i]
isO(1)
on average - Total:
O(E) + O(N) = O(E + N)
Space Complexity: O(N)
where N
is the number of vertices.
- The Counter object
cnt
stores at mostN
unique vertices (in the worst case, every vertex except one has at least one incoming edge):O(N)
- The output list contains vertices with no incoming edges, which in the worst case could be
O(N)
vertices - Total auxiliary space:
O(N)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding the Counter's Default Behavior
A common mistake is assuming you need to explicitly initialize all vertices in the Counter before checking their in-degrees. Some developers might write unnecessary initialization code:
# Unnecessary initialization - DON'T DO THIS
in_degree_count = Counter()
for i in range(n):
in_degree_count[i] = 0 # Redundant!
for source, destination in edges:
in_degree_count[destination] += 1
Why it's a pitfall: Python's Counter returns 0 for missing keys by default, so vertices that never appear as destinations automatically have an in-degree of 0. The extra initialization adds unnecessary complexity and reduces code elegance.
Correct approach: Trust Counter's default behavior:
in_degree_count = Counter(destination for source, destination in edges) # in_degree_count[vertex] returns 0 if vertex is not in the counter
2. Using a Set or List to Track Destinations Instead of Counter
Some might try to collect all destination vertices in a set and then find the difference:
# Inefficient approach - AVOID
destinations = set()
for source, destination in edges:
destinations.add(destination)
return [i for i in range(n) if i not in destinations]
Why it's a pitfall: While this works functionally, it's less clear about the underlying logic (counting in-degrees) and doesn't extend well if you later need to know the actual in-degree values for other purposes.
3. Forgetting to Handle Empty Graphs or Graphs with No Edges
If edges
is empty (no edges in the graph), some implementations might fail:
# Potential issue if not careful
if not edges: # Some might think special handling is needed
return list(range(n)) # This is actually correct but adds unnecessary code
Why it's a pitfall: The original solution already handles this case correctly! When edges
is empty, the Counter is empty, so all vertices have in-degree 0 and all are returned. Adding special case handling makes the code more complex without benefit.
4. Incorrect Variable Naming in the Generator Expression
A subtle but important mistake is using confusing variable names when unpacking edges:
# Confusing naming - AVOID in_degree_count = Counter(from_v for from_v, to_v in edges) # WRONG! # This counts source vertices, not destinations!
Why it's a pitfall: This completely reverses the logic, counting out-degrees instead of in-degrees. The result would be vertices that don't have any outgoing edges, which is not what we want.
Correct approach: Use clear, meaningful names:
in_degree_count = Counter(destination for source, destination in edges) # Or use underscore for unused variables in_degree_count = Counter(to for _, to in edges)
5. Assuming the Graph is Connected
Some might assume that the minimum set should always have size 1 for a connected graph:
# WRONG assumption
result = [vertex for vertex in range(n) if in_degree_count[vertex] == 0]
if len(result) > 1:
# Trying to "optimize" by reducing the set - DON'T DO THIS
return [result[0]] # WRONG!
Why it's a pitfall: A DAG can have multiple disconnected components or parallel branches that all need separate starting points. The problem asks for the minimum set to reach ALL vertices, not just some of them.
Solution: Trust the algorithm - all vertices with in-degree 0 are necessary and sufficient for the minimum starting set.
Which of the following problems can be solved with backtracking (select multiple)
Recommended Readings
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!