3310. Remove Methods From Project | LeetCode Daily Python Solution (two passes, zero extra tools)
š Full written solution: https://interview-kit-fe.vercel.app/remove-methods-from-project š» Code on GitHub: https://github.com/timepasshub2539-hue/leetcode-python/tree/main/remove-methods-from-project Chapters: 0:00 The Problem: Remove Methods From Project 0:18 One Buggy Method Just Took Down Half Your App 0:36 The Real Task: Delete The Bug, Not The Business 0:51 Why This Exact Shape Keeps Showing Up 1:07 Reading The Input: n, k, And The Edges 1:24 The Only Three Tools This Solution Needs 1:46 Turning The Edge List Into A Real Graph 1:58 The Naive Idea (And Where It Breaks) 2:09 Here's The Trick: Reachable From k, Then Check Who Calls In 2:51 Walking Out From k To Mark Everyone It Touches 3:04 Code: Building The Call Graph 3:14 Code: Marking Everyone Suspicious 3:29 Dry Run: Watching The Set Grow 3:45 Pause: Can We Actually Delete Anything Here? 3:59 Reveal: The Incoming-Call Check 4:14 Code: The Complete Solution 4:25 Mistakes That'll Burn You On This Problem 4:42 The Alternative: Recursive DFS Instead Of A Stack 5:00 Complexity: One Pass Per Method And Edge 5:10 Edge Cases This Code Has To Survive 5:27 Reverse The Question: Who Still Calls In? 6:07 Quick Quiz! 6:15 Quick Quiz! 6:18 Answer 6:27 Round 2! 6:35 Round 2! 6:39 Answer Solve LeetCode 3310 Remove Methods From Project in Python: build the call graph from the edge list, find every method reachable from k, then run the incoming-call check that decides whether you can actually delete anything. Includes a full dry run, both stack and recursive DFS versions, complexity breakdown, and the edge cases that trip people up. Quick quizzes at the end to test what you learned. #leetcode #python #coding #dsa #leetcodedaily Watch next: - Lesson 12: The 1,000 Items That Disappeared From My Inner Join: https://youtu.be/5nG6Ofq2PTs - I Built 4 Python Projects in ONE Sitting (Birthday Paradox, Blackjack, Bitmap Message, Bouncing DVD Logo): https://youtu.be/Hxni16TYDs4 - Two Projects, One Broken Package #shorts: https://youtu.be/GZzSXZWZaqo
Introduction
Every large codebase has a version of this problem hiding in it. A function gets flagged as broken, and the real question is never "how do I delete this one function" ā it's "how much of the system does this function's blast radius actually cover, and is any of that blast radius something other code still relies on." That second half is the part people skip, and it's exactly what LeetCode 3310, Remove Methods From Project, is testing.
This problem shows up in interviews because it's really two classic ideas stitched together: graph reachability (find everything downstream of a starting node) and a safety check on that result (make sure nothing outside the group depends on it). Individually, both are things most engineers can do in their sleep. Together, in a 20-30 minute interview window, they trip people up because it's tempting to stop after step one and declare victory.
Once you've worked through this problem, you'll recognize the pattern immediately in real systems: removing a broken microservice and its transitive dependents, deleting a package and everything that imports it, or pruning dead code in a build graph. The mechanics are identical ā flood outward from the source, then check the perimeter before you commit to deleting anything.
Problem Overview
You're given n methods, numbered 0 to n - 1, and an integer k identifying a method that has a bug. You're also given invocations, a list of pairs [a, b] where each pair means "method a calls method b."
Because method k is buggy, every method it calls ā directly or transitively ā is considered suspicious too, since a broken method can corrupt anything downstream of it. Your job is to return the list of methods that remain safe to keep after removing k and its suspicious dependents.
There's one catch: you can only actually remove the suspicious group if nothing outside that group calls into it. If some healthy method still depends on a method inside the suspicious set, removing the group would break that healthy method, so in that case you remove nothing at all and return every method unchanged.
Example
Suppose n = 5 and k = 1, with:
invocations = [[0, 1], [0, 2], [1, 2], [3, 4]]
Reading each pair as caller ā callee: method 0 calls 1 and 2, method 1 calls 2, and method 3 calls 4 (an unrelated pair off on its own).
Starting from k = 1: method 1 calls method 2, so {1, 2} is suspicious. Method 0 is not suspicious itself, but it calls method 1, which is suspicious ā that's an incoming edge from outside the group. Because of that edge, we can't remove anything. The answer is [0, 1, 2, 3, 4], every method, untouched.
Change the starting point to k = 3. Method 3 only calls method 4, so {3, 4} is suspicious. No other method calls into 3 or 4. Nothing blocks the removal, so the answer is [0, 1, 2] ā everything except the suspicious group.
The difference between these two examples is the entire problem: reachability finds the candidates, but the incoming-edge check decides whether you're allowed to act on them.
Intuition
Start by separating the problem into two questions, because trying to solve them at once is where confusion creeps in.
Question one: what's suspicious? This is pure forward reachability. Starting at k, anything you can reach by following "calls" edges is suspicious ā k itself, plus everything it calls, plus everything those calls call, and so on. This is a textbook flood fill: seed a frontier with k, expand outward one hop at a time, and stop when there's nothing new left to visit.
Question two: is it safe to remove? This is where the problem stops being a pure traversal exercise. A method being suspicious doesn't mean it's isolated ā other, perfectly healthy methods might still call into the suspicious group from outside. If you deleted the group anyway, those healthy methods would be left calling into nothing. So before removing anything, you scan every edge in the graph and ask: does this edge start outside the suspicious set and end inside it? If even one edge does, the whole group is off-limits ā not just parts of it, all of it ā because you can't cleanly separate "used" from "buggy" once there's a live dependency crossing the boundary.
The key mental shift is realizing these are two separate passes over the graph, not one combined check. First you flood outward to find who's suspicious. Only after that set is finalized do you look at the graph a second time, checking every edge for the reverse case ā someone healthy pointing in.
Brute Force Solution
Idea: For every method, run a full traversal to determine whether it's reachable from k. Then, for that same suspicious set, iterate over every invocation pair and check if it crosses into the group from outside.
Algorithm:
- Build an adjacency list from
invocations. - Run DFS/BFS from
kto compute the suspicious set ā this part isn't actually "brute force" different from the optimal approach, since flood fill is already the right tool. - For the incoming-edge check, loop over every
[a, b]pair and test membership in the suspicious set with a linear scan through a list instead of a set.
Advantages: Conceptually simple, easy to reason about correctness.
Disadvantages: If membership checks use lists instead of hash sets, each check costs O(size of suspicious set) instead of O(1), pushing the total complexity toward O(n Ć e) in the worst case ā far too slow once n and the edge count approach 100,000, which the constraints allow.
Complexity: Time O(n Ć e) with naive membership checks, Space O(n + e) for the graph.
The fix is almost free: swap the list for a set. That single change turns this "brute force" version into the optimal one, which is why this problem doesn't really have a meaningfully different slow solution ā the brute force mistake is a data structure choice, not an algorithmic one.
Optimal Solution
The optimal solution is the same two-phase idea, done with O(1) membership checks throughout.
Phase 1 ā Build the graph. For every [a, b] in invocations, append b to graph[a]'s call list. This gives you, for any method, the exact list of methods it calls directly.
Phase 2 ā Flood fill from k. Use a set called suspicious and a stack, both seeded with k. Pop a method off the stack, look at everything it calls via graph, and for each callee not already in suspicious, add it to the set and push it onto the stack. Repeat until the stack is empty. When it's empty, suspicious contains exactly the connected group reachable from k.
| Step | Stack | Suspicious set |
|---|---|---|
| Start | [k] |
{k} |
| Pop k, push its callees | [callees of k] |
{k, callees of k} |
| Pop each callee, push new callees | ... | grows until no new nodes found |
| Stack empty | [] |
final suspicious set |
Phase 3 ā Check incoming edges. Iterate over every [a, b] in invocations once. If b is in suspicious and a is not, you've found a healthy method calling into the suspicious group ā removal is blocked, so return every method from 0 to n - 1 unchanged.
Phase 4 ā Build the result. If no blocking edge was found, return every method from 0 to n - 1 that is not in suspicious.
Python Code
def remove_methods(n: int, k: int, invocations: list[list[int]]) -> list[int]:
# Build the call graph: method -> list of methods it calls
graph = [[] for _ in range(n)]
for caller, callee in invocations:
graph[caller].append(callee)
# Phase 1: flood fill from k to find every suspicious method
suspicious = {k}
stack = [k]
while stack:
method = stack.pop()
for callee in graph[method]:
if callee not in suspicious:
suspicious.add(callee)
stack.append(callee)
# Phase 2: block removal if any healthy method calls into the group
for caller, callee in invocations:
if callee in suspicious and caller not in suspicious:
return list(range(n))
# Phase 3: everything outside the suspicious set survives
return [method for method in range(n) if method not in suspicious]
Code Walkthrough
graph = [[] for _ in range(n)]ā one adjacency list slot per method, built once so lookups during the flood fill are O(1).- The first loop populates
graphby appending eachcalleeto itscaller's list ā this is just re-expressing the edge list as an adjacency list. suspicious = {k}andstack = [k]ā a set for instant membership checks, a stack (not recursion) so a long dependency chain can't blow Python's recursion limit.- The
while stackloop is standard iterative DFS: pop a method, examine its direct callees, and push any unseen ones onto the stack after marking them. - The second loop over
invocationsis the safety check ā it's a separate, full pass over every edge, checking specifically for edges that enter the suspicious set from outside it. - Returning
list(range(n))on a blocked removal means "nothing changes" ā every method survives, includingk. - The final list comprehension filters out the suspicious set only when removal was never blocked.
Dry Run
Take n = 5, k = 0, invocations = [[0, 1], [0, 2], [1, 2], [3, 4]] (using the first example's edges but starting from 0 this time).
Build graph: graph[0] = [1, 2], graph[1] = [2], graph[3] = [4], graph[2] = graph[4] = [].
Flood fill: suspicious = {0}, stack = [0].
- Pop
0. Callees1, 2are new āsuspicious = {0, 1, 2},stack = [1, 2]. - Pop
2. No callees.stack = [1]. - Pop
1. Callee2already insuspicious.stack = [].
Final suspicious = {0, 1, 2}.
Incoming edge check: Scan [0,1], [0,2], [1,2] ā callers 0 and 1 are both already inside suspicious, so no blocking edge. [3,4] ā callee 4 isn't in suspicious at all, irrelevant.
Result: No block found, so return methods not in {0, 1, 2} ā [3, 4].
Complexity Analysis
Time: O(n + e), where e is the number of invocations. Building the graph is O(e). The flood fill visits each node at most once and each edge at most once (a node only enters the stack the first time it's discovered), so that's O(n + e). The incoming-edge check is one pass over all edges, O(e). Building the final list is O(n). Everything sums to O(n + e).
Space: O(n + e) for the adjacency list, the suspicious set, and the stack, all of which are bounded by the number of nodes and edges.
These bounds are tight because you fundamentally cannot solve this problem without looking at every method and every edge at least once ā there's no shortcut around examining the full input.
Alternative Solutions
You can write the flood fill recursively instead of with an explicit stack:
def dfs(method, graph, suspicious):
suspicious.add(method)
for callee in graph[method]:
if callee not in suspicious:
dfs(callee, graph, suspicious)
It reads slightly cleaner, but with n up to 100,000, a long linear chain of calls (0 ā 1 ā 2 ā ... ā 99999) can exceed Python's default recursion limit and crash. The stack-based version has identical time and space complexity but never hits that ceiling, so it's the safer default here ā this is one of the rare cases where the "less elegant" version is unambiguously the right call.
BFS with a queue instead of a stack works identically well; the traversal order doesn't matter for this problem, only that every reachable node gets visited exactly once.
Edge Cases
- k calls nothing:
suspicious = {k}only. If nothing points intok, it's removed cleanly by itself. - No invocations at all: The incoming-edge check loop never runs, so nothing can block removal ā
kand anything it (vacuously) calls just gets removed. - The graph has a cycle back through k: If every method ends up reachable from
k(a fully connected or cyclic graph),suspiciousends up being allnmethods, and the result is an empty list ā everything is suspicious, nothing outside exists to point back in. - Duplicate invocation pairs: Duplicates just get added to
graph[caller]more than once; they don't change correctness, though a defensive implementation could dedupe them for a small constant-factor speedup. - k is unreachable from anything but has huge fan-out: The flood fill still correctly captures the whole downstream set regardless of how large it is; complexity scales with the number of edges touched, not with
nalone.
Common Mistakes
- Skipping the incoming-edge check entirely and always removing the suspicious set ā this passes on graphs where nothing calls back in, then silently fails the moment a healthy caller exists.
- Removing only part of the suspicious group when a blocking edge is found, instead of blocking the removal completely. The rule is all-or-nothing: one bad edge cancels the entire operation.
- Using plain recursion for the traversal without considering
nup to 100,000 ā a deep call chain will hit Python's recursion limit. - Forgetting that k itself is suspicious, not just what it calls ā some implementations start the suspicious set empty and only add k's callees, missing k.
- Checking membership with a list instead of a set, turning an O(n + e) solution into something quadratic under the hood without any visible change to the algorithm's structure.
Interview Questions
- How would you extend this to handle multiple simultaneously buggy starting methods?
- What changes if the graph is undirected instead of directed?
- How would you modify this to also report why removal was blocked ā i.e., return the specific blocking edge?
- Could you solve the incoming-edge check without a second full pass over
invocations? (You can fold it into the flood fill if you also track reverse edges, but it doesn't improve asymptotic complexity.) - How does this generalize to strongly connected components in a general dependency graph?
Similar Problems
- LeetCode 802 (Find Eventual Safe States) ā also about reachability and separating "safe" nodes from a computed unsafe set.
- LeetCode 207 / 210 (Course Schedule I & II) ā dependency graph traversal with cycle detection, same adjacency-list-plus-DFS backbone.
- LeetCode 1319 (Number of Operations to Make Network Connected) ā connected component counting, same flood-fill mechanics.
- LeetCode 947 (Most Stones Removed with Same Row or Column) ā reachability groups determine what can safely be removed, a close conceptual cousin.
Key Takeaways
Reachability alone answers "what's connected to the source," but real deletion problems almost always add a second question: "does anything outside still depend on this group." Solve those as two clean, separate passes ā flood fill first, then a perimeter check ā rather than trying to merge them into one clever loop. And whenever n can get large, default to iterative traversal over recursion; it costs nothing and removes an entire class of crash.
Watch the Video
For the full walkthrough with the graph drawn out live, both the stack and recursive DFS versions compared side by side, and the quick quizzes at the end to test yourself, watch the video here: {LINK}
About the Series
This is part of Fun with Learning Technology, a series that takes the interview problems that trip people up and breaks them down until the pattern actually clicks ā not just for this one problem, but for the next ten that look like it.
Call To Action
If this walkthrough helped, the biggest support is a like on the video and a subscribe so future breakdowns land in your feed. For deeper dives and written companions to every problem, subscribe to the Substack. And if something in this explanation could be clearer, or you want a specific problem covered next, drop it in the comments ā that feedback shapes what gets made next.
The solution
def remainingMethods(n, k, invocations):
graph = defaultdict(list)
for a, b in invocations:
graph[a].append(b)
suspicious, stack = {k}, [k]
while stack:
node = stack.pop()
for nxt in graph[node]:
if nxt not in suspicious:
suspicious.add(nxt); stack.append(nxt)
for a, b in invocations:
if a not in suspicious and b in suspicious:
return list(range(n))
return [i for i in range(n) if i not in suspicious]Ready to try it yourself? Solve Graph problems with instant feedback in the practice sandbox.
Practice now ā


