You are maintaining a project that has n methods numbered from 0 to n - 1.
You are given two integers n and k, and a 2D integer array invocations, where invocations[i] = [ai, bi] indicates that method ai invokes method bi.
There is a known bug in method k. Method k, along with any method invoked by it, either directly or indirectly, are considered suspicious and we aim to remove them.
A group of methods can only be removed if no method outside the group invokes any methods within it.
Return an array containing all the remaining methods after removing all the suspicious methods. You may return the answer in any order. If it is not possible to remove all the suspicious methods, none should be removed.
Example 1:
Input: n = 4, k = 1, invocations = [[1,2],[0,1],[3,2]]
Output: [0,1,2,3]
Explanation:

Method 2 and method 1 are suspicious, but they are directly invoked by methods 3 and 0, which are not suspicious. We return all elements without removing anything.
Example 2:
Input: n = 5, k = 0, invocations = [[1,2],[0,2],[0,1],[3,4]]
Output: [3,4]
Explanation:

Methods 0, 1, and 2 are suspicious and they are not directly invoked by any other method. We can remove them.
Example 3:
Input: n = 3, k = 2, invocations = [[1,2],[0,1],[2,0]]
Output: []
Explanation:

All methods are suspicious. We can remove them.
Constraints:
1 <= n <= 1050 <= k <= n - 10 <= invocations.length <= 2 * 105invocations[i] == [ai, bi]0 <= ai, bi <= n - 1ai != biinvocations[i] != invocations[j]When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:
The approach is to start with the methods we're initially told to remove and then repeatedly scan the entire project. In each scan, we find other methods that have become useless because all the methods that depended on them are also being removed.
Here's how the algorithm would work step-by-step:
def remove_methods_brute_force(num_methods, methods_to_remove, dependencies):
all_method_indices = range(num_methods)
min_broken_methods = float('inf')
best_group_to_remove = []
# To meet the brute-force requirement, we must generate every single possible group of methods to remove.
all_possible_groups = itertools.combinations(all_method_indices, methods_to_remove)
for group_to_remove in all_possible_groups:
removed_methods_set = set(group_to_remove)
current_broken_count = 0
# We iterate through all methods to check which ones remain and might be broken by the removal.
for method_index in all_method_indices:
if method_index in removed_methods_set:
continue
is_method_broken = False
# A method is broken if any of its direct dependencies are in the group slated for removal.
for dependency in dependencies[method_index]:
if dependency in removed_methods_set:
is_method_broken = True
break
if is_method_broken:
current_broken_count += 1
# If the current group results in fewer broken methods, it becomes our new best solution.
if current_broken_count < min_broken_methods:
min_broken_methods = current_broken_count
best_group_to_remove = list(group_to_remove)
return best_group_to_removeInstead of directly figuring out which methods to remove, the trick is to flip the problem. We find the largest possible group of methods that can work together without any circular dependencies. Any method left out of this group is one that must be removed.
Here's how the algorithm would work step-by-step:
def remove_methods_from_project(methods, dependencies):
memoization_cache = {}
def _find_one_cycle(nodes_in_subgraph, graph):
path_parents = {}
visited_nodes = set()
for start_node in nodes_in_subgraph:
if start_node in visited_nodes:
continue
recursion_stack_nodes = set()
dfs_stack = [(start_node, iter(graph.get(start_node, [])))]
visited_nodes.add(start_node)
while dfs_stack:
current_node, children_iterator = dfs_stack[-1]
recursion_stack_nodes.add(current_node)
try:
child_node = next(children_iterator)
if child_node not in nodes_in_subgraph:
continue
if child_node in recursion_stack_nodes:
cycle_path = [child_node]
parent = current_node
while parent != child_node:
cycle_path.append(parent)
parent = path_parents[parent]
cycle_path.reverse()
return cycle_path
if child_node not in visited_nodes:
visited_nodes.add(child_node)
path_parents[child_node] = current_node
dfs_stack.append((child_node, iter(graph.get(child_node, []))))
except StopIteration:
recursion_stack_nodes.remove(current_node)
dfs_stack.pop()
return []
def _solve_recursive(current_methods_tuple):
if current_methods_tuple in memoization_cache:
return memoization_cache[current_methods_tuple]
current_methods_set = set(current_methods_tuple)
if not current_methods_set:
return 0
graph = {method: [] for method in current_methods_set}
reverse_graph = {method: [] for method in current_methods_set}
out_degree = {method: 0 for method in current_methods_set}
in_degree = {method: 0 for method in current_methods_set}
for caller, callee in dependencies:
if caller in current_methods_set and callee in current_methods_set:
graph[caller].append(callee)
reverse_graph[callee].append(caller)
out_degree[caller] += 1
in_degree[callee] += 1
# Pruning non-cyclic methods drastically reduces the search space, focusing only on the core problem.
simplification_queue = [
method for method in current_methods_set if in_degree[method] == 0 or out_degree[method] == 0
]
tangled_methods = set(current_methods_set)
queue_head_index = 0
while queue_head_index < len(simplification_queue):
method_to_remove = simplification_queue[queue_head_index]
queue_head_index += 1
if method_to_remove not in tangled_methods:
continue
tangled_methods.remove(method_to_remove)
for callee_neighbor in graph.get(method_to_remove, []):
if callee_neighbor in tangled_methods:
in_degree[callee_neighbor] -= 1
if in_degree[callee_neighbor] == 0:
simplification_queue.append(callee_neighbor)
for caller_neighbor in reverse_graph.get(method_to_remove, []):
if caller_neighbor in tangled_methods:
out_degree[caller_neighbor] -= 1
if out_degree[caller_neighbor] == 0:
simplification_queue.append(caller_neighbor)
if not tangled_methods:
return 0
# By breaking one cycle, we guarantee progress, and exploring its nodes as removal candidates is sufficient.
cycle_to_break = _find_one_cycle(tangled_methods, graph)
min_removals = float('inf')
# We explore removing each node in the found cycle, as this guarantees breaking it in an optimal way.
for method_in_cycle in cycle_to_break:
next_methods = list(current_methods_set)
next_methods.remove(method_in_cycle)
next_methods_tuple = tuple(sorted(next_methods))
removals_for_branch = 1 + _solve_recursive(next_methods_tuple)
min_removals = min(min_removals, removals_for_branch)
# Caching results for a given set of methods prevents re-solving identical subproblems.
memoization_cache[current_methods_tuple] = min_removals
return min_removals
initial_methods_tuple = tuple(sorted(list(methods)))
return _solve_recursive(initial_methods_tuple)| Case | How to Handle |
|---|---|
| The 'invocations' array is empty. | The only suspicious method is k, which has no incoming edges, so it is always removed and the remaining n-1 methods are returned. |
| The method invocation graph is disconnected. | The traversal to find suspicious methods correctly explores only k's component, and the validity check properly considers invocations from other components. |
| The buggy method k is part of a dependency cycle. | A graph traversal using a visited set correctly identifies all methods in the cycle and those reachable from it as suspicious without getting into an infinite loop. |
| All methods in the project are suspicious. | The check for external invocations becomes trivial as no non-suspicious methods exist, so all methods are removed and an empty array is returned. |
| A non-suspicious method invokes a suspicious one. | This is the primary failure condition, causing the entire removal operation to be aborted and all n methods to be returned. |
| The buggy method k has no outgoing invocations. | The set of suspicious methods will only contain k, and its removal depends solely on whether any other method invokes it. |
| The buggy method k is an isolated node with no invocations. | The suspicious set only contains k, and since it has no incoming edges from other methods, it is always removed. |
| Large inputs with maximum n and number of invocations. | An efficient graph-based solution with O(n + |invocations|) complexity is required to avoid a timeout. |