3310. Remove Methods From Project
Problem Description
You have a project with n
methods numbered from 0
to n - 1
. You're given:
- Two integers
n
andk
- A 2D array
invocations
whereinvocations[i] = [a_i, b_i]
means methoda_i
calls methodb_i
Method k
contains a known bug. Any method that k
invokes (directly or indirectly through a chain of invocations) is considered suspicious. Your goal is to remove all suspicious methods from the project.
The key constraint: You can only remove the suspicious methods if no non-suspicious method invokes any suspicious method. In other words, the group of suspicious methods must be isolated - they can call each other, but no method outside the group should call any method inside the group.
What to return:
- If it's possible to remove all suspicious methods without breaking the constraint, return an array of all remaining (non-suspicious) methods in any order
- If it's impossible to remove all suspicious methods (because some non-suspicious method depends on them), return all methods unchanged
Example scenario: If method k=2
calls method 3
, and method 3
calls method 4
, then methods 2
, 3
, and 4
are all suspicious. These can only be removed if no other methods (like 0
, 1
, 5
, etc.) call any of 2
, 3
, or 4
.
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 involves methods (nodes) and invocations between them (edges). The invocations array explicitly defines directed edges where
[a, b]
means methoda
calls methodb
, forming a directed graph structure.
Is it a tree?
- No: The graph can have cycles (a method can indirectly call itself through other methods) and multiple methods can invoke the same method, so it's not a tree structure.
Is the problem related to directed acyclic graphs?
- No: While the graph is directed, the problem doesn't specifically require it to be acyclic. The focus is on finding reachable nodes and checking connectivity, not on DAG-specific properties.
Is the problem related to shortest paths?
- No: We're not finding shortest paths between methods. Instead, we need to find all methods reachable from method
k
(to identify suspicious methods) and verify isolation.
Does the problem involve connectivity?
- Yes: The core of the problem is about connectivity - finding all methods connected to method
k
(suspicious methods) and checking if this group is isolated from non-suspicious methods.
Disjoint Set Union?
- No: While DSU could potentially be used, the problem requires traversing from specific starting points and understanding directional relationships, which is better suited for DFS.
Does the problem have small constraints?
- Yes: With methods numbered from
0
ton-1
and the need to explore reachability, the constraints are manageable for traversal algorithms.
DFS/backtracking?
- Yes: DFS is perfect for this problem as we need to:
- Traverse from method
k
to find all suspicious methods (following the invocation edges) - Traverse from non-suspicious methods to check if they can reach any suspicious methods
- Traverse from method
Conclusion: The flowchart leads us to use DFS (Depth-First Search) for solving this problem, which aligns perfectly with the solution that uses two DFS traversals - one to identify suspicious methods and another to verify isolation.
Intuition
The problem asks us to remove a "contaminated" group of methods starting from method k
. Think of it like a virus spreading through method calls - if method k
is infected, any method it calls becomes infected too, and this spreads transitively.
The key insight is that we can only remove these infected methods if they form an isolated group. If any healthy method depends on an infected one, we can't remove the infected methods without breaking the healthy code.
This naturally leads to a two-phase approach:
Phase 1: Find all suspicious methods
Starting from method k
, we need to find all methods that are reachable by following the invocation edges. This is like coloring all nodes reachable from k
as "suspicious". We use DFS to traverse the graph following the direction of invocations (a β b
means if a
is suspicious, b
becomes suspicious too).
Phase 2: Check for external dependencies Now we need to verify if any non-suspicious method calls a suspicious one. If this happens, we can't remove the suspicious methods. The clever approach here is to start from all non-suspicious methods and see if they can reach any suspicious methods. If they can, those suspicious methods cannot be removed (we mark them back as non-suspicious).
Why does this work? Because if a non-suspicious method x
can reach a suspicious method y
, it means x
depends on y
. Removing y
would break x
. The solution uses an undirected graph representation (f
) in the second DFS to efficiently check these dependencies - if we can traverse from a non-suspicious method to a suspicious one through any path, we know there's a dependency.
The two-DFS pattern elegantly solves both requirements: identifying the infected group and verifying its isolation. If after both phases some methods remain marked as suspicious, they form a removable isolated group.
Learn more about Depth-First Search, Breadth-First Search and Graph patterns.
Solution Approach
The solution implements the two-DFS strategy to identify and validate the removal of suspicious methods.
Data Structure Setup:
f
: An adjacency list representing an undirected graph for checking connectivity between all methodsg
: An adjacency list representing the directed graph of method invocationssuspicious
: A boolean array marking which methods are suspiciousvis
: A boolean array for tracking visited nodes in the second DFS
Step 1: Build the Graph Representations
f = [[] for _ in range(n)] # Undirected graph
g = [[] for _ in range(n)] # Directed graph
for a, b in invocations:
f[a].append(b)
f[b].append(a) # Both directions for connectivity check
g[a].append(b) # Only forward direction for finding suspicious methods
The directed graph g
follows the actual invocation relationships, while the undirected graph f
helps us check if there's any connection between non-suspicious and suspicious methods.
Step 2: First DFS - Mark All Suspicious Methods
def dfs(i: int):
suspicious[i] = True
for j in g[i]:
if not suspicious[j]:
dfs(j)
Starting from method k
, we traverse the directed graph g
to mark all reachable methods as suspicious. This captures the transitive nature of the bug spreading through method calls.
Step 3: Second DFS - Validate Isolation
def dfs2(i: int):
vis[i] = True
for j in f[i]:
if not vis[j]:
suspicious[j] = False # Found a path from non-suspicious to suspicious
dfs2(j)
For each non-suspicious method, we perform DFS on the undirected graph f
. If we can reach a suspicious method from a non-suspicious one, it means there's a dependency that prevents removal. We mark such suspicious methods back to False
.
Step 4: Collect Results
for i in range(n):
if not suspicious[i] and not vis[i]:
dfs2(i)
return [i for i in range(n) if not suspicious[i]]
After both DFS passes, methods that remain marked as non-suspicious are the ones we keep. If any suspicious method was found to have external dependencies, all suspicious methods would have been marked back to non-suspicious, effectively keeping all methods.
The algorithm runs in O(n + m)
time where m
is the number of invocations, as we traverse each edge and node a constant number of times.
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 illustrate the solution approach.
Example Setup:
n = 5
(methods 0 through 4)k = 2
(method 2 has the bug)invocations = [[2,3], [3,4], [0,1], [1,2]]
This creates the following call structure:
0 β 1 β 2 β 3 β 4
Step 1: Build Graph Representations
Directed graph g
(follows actual invocations):
g[0] = [1]
g[1] = [2]
g[2] = [3]
g[3] = [4]
g[4] = []
Undirected graph f
(for connectivity checking):
f[0] = [1]
f[1] = [0, 2]
f[2] = [1, 3]
f[3] = [2, 4]
f[4] = [3]
Step 2: First DFS - Find Suspicious Methods
Starting from k = 2
:
- Mark method 2 as suspicious
- Method 2 calls method 3, so mark 3 as suspicious
- Method 3 calls method 4, so mark 4 as suspicious
After first DFS: suspicious = [False, False, True, True, True]
Methods 2, 3, and 4 are marked as suspicious.
Step 3: Second DFS - Check External Dependencies
Now we check if any non-suspicious method (0 or 1) can reach a suspicious method:
Starting from method 0 (non-suspicious):
- Visit method 0, mark
vis[0] = True
- Method 0 connects to method 1 in undirected graph
- Visit method 1, mark
vis[1] = True
- Method 1 connects to methods 0 (already visited) and 2
- Method 2 is suspicious! This means non-suspicious method 1 depends on suspicious method 2
- Mark method 2 as non-suspicious:
suspicious[2] = False
- Continue DFS from method 2...
- Eventually mark methods 3 and 4 as non-suspicious too
After second DFS: suspicious = [False, False, False, False, False]
Step 4: Collect Results
Since external dependencies were found (method 1 calls method 2), we cannot remove any methods.
Return: [0, 1, 2, 3, 4]
(all methods remain)
Alternative Scenario:
If the invocations were [[2,3], [3,4], [0,1]]
instead (removing the [1,2]
edge):
- After first DFS: Methods 2, 3, 4 would be suspicious
- After second DFS: No non-suspicious method can reach the suspicious group
- Return:
[0, 1]
(successfully removed the isolated suspicious methods)
Solution Implementation
1class Solution:
2 def remainingMethods(
3 self, n: int, k: int, invocations: List[List[int]]
4 ) -> List[int]:
5 # Build adjacency lists for the graph
6 # directed_graph: for following method invocation chains
7 # undirected_graph: for bidirectional connectivity check
8 directed_graph = [[] for _ in range(n)]
9 undirected_graph = [[] for _ in range(n)]
10
11 for caller, callee in invocations:
12 directed_graph[caller].append(callee)
13 undirected_graph[caller].append(callee)
14 undirected_graph[callee].append(caller)
15
16 # Track which methods are suspicious (potentially buggy)
17 is_suspicious = [False] * n
18
19 def mark_suspicious_methods(method_id: int) -> None:
20 """
21 DFS to mark all methods reachable from the buggy method as suspicious.
22 These are methods that could be affected by the bug.
23 """
24 is_suspicious[method_id] = True
25 for next_method in directed_graph[method_id]:
26 if not is_suspicious[next_method]:
27 mark_suspicious_methods(next_method)
28
29 # Mark all methods reachable from the buggy method k
30 mark_suspicious_methods(k)
31
32 # Track visited methods in the second DFS
33 visited = [False] * n
34
35 def unmark_connected_suspicious(method_id: int) -> None:
36 """
37 DFS from non-suspicious methods to unmark suspicious methods
38 that are connected to the non-suspicious part of the graph.
39 This identifies suspicious methods that cannot be safely removed.
40 """
41 visited[method_id] = True
42 for connected_method in undirected_graph[method_id]:
43 if not visited[connected_method]:
44 is_suspicious[connected_method] = False
45 unmark_connected_suspicious(connected_method)
46
47 # Check each non-suspicious method and unmark connected suspicious methods
48 for method_id in range(n):
49 if not is_suspicious[method_id] and not visited[method_id]:
50 unmark_connected_suspicious(method_id)
51
52 # Return all methods that are not suspicious (can remain in the system)
53 return [method_id for method_id in range(n) if not is_suspicious[method_id]]
54
1class Solution {
2 private boolean[] isSuspicious;
3 private boolean[] isVisited;
4 private List<Integer>[] undirectedGraph;
5 private List<Integer>[] directedGraph;
6
7 public List<Integer> remainingMethods(int n, int k, int[][] invocations) {
8 // Initialize arrays and adjacency lists
9 isSuspicious = new boolean[n];
10 isVisited = new boolean[n];
11 undirectedGraph = new List[n];
12 directedGraph = new List[n];
13
14 // Create empty lists for each node
15 Arrays.setAll(undirectedGraph, i -> new ArrayList<>());
16 Arrays.setAll(directedGraph, i -> new ArrayList<>());
17
18 // Build both directed and undirected graphs from invocations
19 for (int[] invocation : invocations) {
20 int caller = invocation[0];
21 int callee = invocation[1];
22
23 // Undirected graph: add edges in both directions
24 undirectedGraph[caller].add(callee);
25 undirectedGraph[callee].add(caller);
26
27 // Directed graph: add edge from caller to callee only
28 directedGraph[caller].add(callee);
29 }
30
31 // Mark all methods reachable from the initial suspicious method k
32 markSuspiciousFromSource(k);
33
34 // Check if any non-suspicious method can reach suspicious methods
35 // If so, mark those suspicious methods as safe (not removable)
36 for (int method = 0; method < n; ++method) {
37 if (!isSuspicious[method] && !isVisited[method]) {
38 markConnectedAsSafe(method);
39 }
40 }
41
42 // Collect all non-suspicious methods as the result
43 List<Integer> remainingMethodsList = new ArrayList<>();
44 for (int method = 0; method < n; ++method) {
45 if (!isSuspicious[method]) {
46 remainingMethodsList.add(method);
47 }
48 }
49
50 return remainingMethodsList;
51 }
52
53 /**
54 * DFS to mark all methods reachable from a suspicious source
55 * Uses directed graph to follow call chain
56 */
57 private void markSuspiciousFromSource(int currentMethod) {
58 isSuspicious[currentMethod] = true;
59
60 // Traverse all methods called by the current suspicious method
61 for (int calledMethod : directedGraph[currentMethod]) {
62 if (!isSuspicious[calledMethod]) {
63 markSuspiciousFromSource(calledMethod);
64 }
65 }
66 }
67
68 /**
69 * DFS to mark methods as safe if they're connected to non-suspicious methods
70 * Uses undirected graph to check connectivity
71 */
72 private void markConnectedAsSafe(int currentMethod) {
73 isVisited[currentMethod] = true;
74
75 // Check all connected methods (both directions)
76 for (int connectedMethod : undirectedGraph[currentMethod]) {
77 if (!isVisited[connectedMethod]) {
78 // If connected to a non-suspicious method, mark as safe
79 isSuspicious[connectedMethod] = false;
80 markConnectedAsSafe(connectedMethod);
81 }
82 }
83 }
84}
85
1class Solution {
2public:
3 vector<int> remainingMethods(int n, int k, vector<vector<int>>& invocations) {
4 // Track which methods are suspicious (potentially buggy)
5 vector<bool> isSuspicious(n, false);
6
7 // Track visited nodes for the second DFS
8 vector<bool> visited(n, false);
9
10 // Adjacency lists for the graph
11 vector<vector<int>> undirectedGraph(n); // Bidirectional edges
12 vector<vector<int>> directedGraph(n); // Original directed edges
13
14 // Build both directed and undirected graphs from invocations
15 for (const auto& invocation : invocations) {
16 int caller = invocation[0];
17 int callee = invocation[1];
18
19 // Build undirected graph (both directions)
20 undirectedGraph[caller].push_back(callee);
21 undirectedGraph[callee].push_back(caller);
22
23 // Build directed graph (original direction only)
24 directedGraph[caller].push_back(callee);
25 }
26
27 // First DFS: Mark all methods reachable from the buggy method k as suspicious
28 // Following the directed edges (caller -> callee)
29 auto markSuspiciousFromBuggy = [&](this auto&& markSuspiciousFromBuggy, int currentMethod) -> void {
30 isSuspicious[currentMethod] = true;
31
32 // Mark all methods that this method calls as suspicious
33 for (int calledMethod : directedGraph[currentMethod]) {
34 if (!isSuspicious[calledMethod]) {
35 markSuspiciousFromBuggy(calledMethod);
36 }
37 }
38 };
39
40 // Start marking suspicious methods from the known buggy method k
41 markSuspiciousFromBuggy(k);
42
43 // Second DFS: Starting from non-suspicious methods, traverse the undirected graph
44 // If we can reach suspicious methods from non-suspicious ones,
45 // those suspicious methods cannot be removed (they're called by safe code)
46 auto markConnectedAsNonRemovable = [&](this auto&& markConnectedAsNonRemovable, int currentMethod) -> void {
47 visited[currentMethod] = true;
48
49 // Traverse all connected methods in the undirected graph
50 for (int connectedMethod : undirectedGraph[currentMethod]) {
51 if (!visited[connectedMethod]) {
52 // If we reach a suspicious method from a non-suspicious one,
53 // mark it as non-suspicious (cannot be removed)
54 isSuspicious[connectedMethod] = false;
55 markConnectedAsNonRemovable(connectedMethod);
56 }
57 }
58 };
59
60 // Check all non-suspicious methods and mark their connected components
61 for (int methodId = 0; methodId < n; ++methodId) {
62 if (!isSuspicious[methodId] && !visited[methodId]) {
63 markConnectedAsNonRemovable(methodId);
64 }
65 }
66
67 // Collect all methods that are not suspicious (these are the remaining methods)
68 vector<int> remainingMethodsList;
69 for (int methodId = 0; methodId < n; ++methodId) {
70 if (!isSuspicious[methodId]) {
71 remainingMethodsList.push_back(methodId);
72 }
73 }
74
75 return remainingMethodsList;
76 }
77};
78
1/**
2 * Finds remaining methods after removing suspicious methods
3 * @param n - Total number of methods
4 * @param k - The initial suspicious method
5 * @param invocations - Array of method invocation relationships [a, b] means method a invokes method b
6 * @returns Array of method indices that are not suspicious
7 */
8function remainingMethods(n: number, k: number, invocations: number[][]): number[] {
9 // Track which methods are suspicious
10 const isSuspicious: boolean[] = Array(n).fill(false);
11
12 // Track visited nodes during second DFS traversal
13 const isVisited: boolean[] = Array(n).fill(false);
14
15 // Bidirectional adjacency list for all invocations
16 const bidirectionalGraph: number[][] = Array.from({ length: n }, () => []);
17
18 // Directed adjacency list following invocation direction
19 const directedGraph: number[][] = Array.from({ length: n }, () => []);
20
21 // Build both graph representations
22 for (const [invoker, invoked] of invocations) {
23 bidirectionalGraph[invoker].push(invoked);
24 bidirectionalGraph[invoked].push(invoker);
25 directedGraph[invoker].push(invoked);
26 }
27
28 /**
29 * First DFS: Mark all methods reachable from the initial suspicious method
30 * Following the directed invocation graph
31 * @param methodIndex - Current method being processed
32 */
33 const markSuspiciousMethods = (methodIndex: number): void => {
34 isSuspicious[methodIndex] = true;
35
36 // Traverse all methods invoked by current method
37 for (const invokedMethod of directedGraph[methodIndex]) {
38 if (!isSuspicious[invokedMethod]) {
39 markSuspiciousMethods(invokedMethod);
40 }
41 }
42 };
43
44 /**
45 * Second DFS: Clear suspicious flag for methods connected to non-suspicious ones
46 * Using bidirectional connections to find mixed components
47 * @param methodIndex - Current method being processed
48 */
49 const clearConnectedMethods = (methodIndex: number): void => {
50 isVisited[methodIndex] = true;
51
52 // Check all bidirectionally connected methods
53 for (const connectedMethod of bidirectionalGraph[methodIndex]) {
54 if (!isVisited[connectedMethod]) {
55 isSuspicious[connectedMethod] = false;
56 clearConnectedMethods(connectedMethod);
57 }
58 }
59 };
60
61 // Step 1: Mark all methods reachable from the initial suspicious method
62 markSuspiciousMethods(k);
63
64 // Step 2: For each non-suspicious method, clear all connected methods
65 // This handles cases where suspicious and non-suspicious methods are interconnected
66 for (let methodIndex = 0; methodIndex < n; methodIndex++) {
67 if (!isSuspicious[methodIndex] && !isVisited[methodIndex]) {
68 clearConnectedMethods(methodIndex);
69 }
70 }
71
72 // Return indices of all non-suspicious methods
73 return Array.from({ length: n }, (_, index) => index)
74 .filter(index => !isSuspicious[index]);
75}
76
Time and Space Complexity
The time complexity is O(n + m)
, where n
is the number of methods and m
is the number of invocations (edges in the graph).
- Building the adjacency lists
f
andg
takesO(m)
time as we iterate through all invocations once. - The first DFS (
dfs
) visits each suspicious method at most once, takingO(n + m)
time in the worst case (visiting all nodes and traversing all edges in graphg
). - The second DFS (
dfs2
) also visits each node at most once and traverses edges in graphf
, takingO(n + m)
time. - The final list comprehension to build the result takes
O(n)
time. - Overall:
O(m) + O(n + m) + O(n + m) + O(n) = O(n + m)
The space complexity is O(n + m)
.
- The adjacency lists
f
andg
store all edges, requiringO(m)
space total. - The
suspicious
array usesO(n)
space. - The
vis
array usesO(n)
space. - The recursion stack for DFS can go up to
O(n)
depth in the worst case. - The result list
ans
could contain up toO(n)
elements. - Overall:
O(m) + O(n) + O(n) + O(n) + O(n) = O(n + m)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall: Misunderstanding the Isolation Check Logic
The Problem: A common mistake is incorrectly implementing the isolation check in the second DFS. The critical error occurs when developers check if a suspicious method can reach a non-suspicious one, rather than checking if a non-suspicious method can reach a suspicious one.
Incorrect Implementation:
def check_isolation(method_id: int) -> None:
visited[method_id] = True
for connected_method in undirected_graph[method_id]:
if not visited[connected_method]:
if is_suspicious[connected_method]: # Wrong check!
# This doesn't properly validate isolation
is_suspicious[method_id] = False
check_isolation(connected_method)
# Starting from suspicious methods (WRONG!)
for method_id in range(n):
if is_suspicious[method_id] and not visited[method_id]:
check_isolation(method_id)
Why This Fails: This approach starts from suspicious methods and tries to find non-suspicious ones. However, it doesn't correctly identify when non-suspicious methods depend on suspicious ones. Consider this scenario:
- Method 0 (non-suspicious) calls method 1 (suspicious)
- Method 1 calls method 2 (suspicious)
- Starting from method 1 won't detect that method 0 depends on it
The Correct Approach:
def unmark_connected_suspicious(method_id: int) -> None:
visited[method_id] = True
for connected_method in undirected_graph[method_id]:
if not visited[connected_method]:
# Unmark ANY method reachable from non-suspicious ones
is_suspicious[connected_method] = False
unmark_connected_suspicious(connected_method)
# Start ONLY from non-suspicious methods
for method_id in range(n):
if not is_suspicious[method_id] and not visited[method_id]:
unmark_connected_suspicious(method_id)
Key Insight: The solution works by starting from non-suspicious methods and exploring all reachable methods. Any suspicious method that can be reached from a non-suspicious one must be preserved (unmarked as suspicious) because removing it would break the non-suspicious method's dependencies. This ensures that we only remove truly isolated groups of suspicious methods.
Test Case to Validate:
n = 4 k = 2 invocations = [[0, 1], [1, 2], [2, 3]] # Method 0 is non-suspicious but calls method 1 (suspicious) # Expected: [0, 1, 2, 3] (cannot remove any methods) # Wrong approach might incorrectly return [0]
Which technique can we use to find the middle of a linked list?
Recommended Readings
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
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
Want a Structured Path to Master System Design Too? Donβt Miss This!