Facebook Pixel

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 and k
  • A 2D array invocations where invocations[i] = [a_i, b_i] means method a_i calls method b_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 method a calls method b, 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 to n-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:
    1. Traverse from method k to find all suspicious methods (following the invocation edges)
    2. Traverse from non-suspicious methods to check if they can reach any suspicious methods

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 methods
  • g: An adjacency list representing the directed graph of method invocations
  • suspicious: A boolean array marking which methods are suspicious
  • vis: 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 Evaluator

Example 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:

  1. Mark method 2 as suspicious
  2. Method 2 calls method 3, so mark 3 as suspicious
  3. 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):

  1. Visit method 0, mark vis[0] = True
  2. Method 0 connects to method 1 in undirected graph
  3. Visit method 1, mark vis[1] = True
  4. Method 1 connects to methods 0 (already visited) and 2
  5. Method 2 is suspicious! This means non-suspicious method 1 depends on suspicious method 2
  6. Mark method 2 as non-suspicious: suspicious[2] = False
  7. Continue DFS from method 2...
  8. 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 and g takes O(m) time as we iterate through all invocations once.
  • The first DFS (dfs) visits each suspicious method at most once, taking O(n + m) time in the worst case (visiting all nodes and traversing all edges in graph g).
  • The second DFS (dfs2) also visits each node at most once and traverses edges in graph f, taking O(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 and g store all edges, requiring O(m) space total.
  • The suspicious array uses O(n) space.
  • The vis array uses O(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 to O(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]
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which technique can we use to find the middle of a linked list?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More