Overview
Most algorithmic bugs are not exotic — they are the same handful of mistakes repeated across problems: a greedy choice applied where it does not hold, an off-by-one in a DP table, a missing memoization cache turning polynomial work into exponential work, or a base case that quietly breaks recursion. This topic walks through the pitfalls that show up most often in interviews and real code, with the reasoning needed to spot and avoid each one.
Cricket analogy: Most collapses aren't due to a rare unplayable delivery but the same repeated mistakes: going for a big shot too early, a greedy choice misapplied, miscounting overs remaining, an off-by-one, or forgetting to note a bowler's spell length, a missing cache.
Frequently Asked Questions
Q: What happens when you apply a greedy algorithm to a problem that actually needs dynamic programming?
You get a solution that looks correct on the examples you tried but fails on a case where the locally best choice is not globally optimal. The classic example is the 0/1 knapsack problem: greedily picking items by best value-to-weight ratio can fail because you cannot take a fractional item, so an earlier greedy pick can block a better later combination. Fractional knapsack, in contrast, does allow taking fractions, which restores the greedy-choice property. The fix is to check whether making the current best choice ever needs to be undone later; if yes, DP is required to consider all reachable states.
Cricket analogy: Greedily picking the batter with the best strike rate per ball can fail in a fixed-overs chase since you can't use a fraction of an over from them; an earlier greedy pick can block a better late combination, unlike a format allowing partial overs.
Q: What is an off-by-one error in a DP table, and why is it so common?
DP tables are often sized (n+1) x (m+1) to hold a base case row/column representing empty prefixes, but it is easy to index with the wrong offset — using dp[i][j] where dp[i-1][j-1] was intended, or looping range(n) instead of range(1, n+1). This is especially common in longest common subsequence and edit distance, where dp[i][j] represents the first i characters of one string and the first j of another, not indices i and j directly. The fix is to write out the exact meaning of dp[i][j] as a sentence before coding, and verify it against a tiny hand-traced example (like two 2-character strings) before trusting the loop bounds.
Cricket analogy: Building a scorecard table sized for overs bowled so far but indexing it as the raw over number instead of over-count-plus-one is a classic off-by-one, the same as LCS tables confusing dp[i][j] with dp[i-1][j-1].
Q: How does exponential blowup happen from a missing memoization cache?
A recursive solution to a problem with overlapping subproblems, written without caching, recomputes the same subproblem exponentially many times. Naive recursive Fibonacci is the textbook case: fib(n) calls fib(n-1) and fib(n-2), and the call tree has O(2^n) nodes even though there are only n distinct subproblems. The same trap appears in naive recursive solutions to coin change, longest increasing subsequence, or matrix chain multiplication. The fix is to add a memo dictionary keyed by the subproblem's parameters (or convert to bottom-up tabulation), turning O(2^n) into O(n) or O(n^2) depending on the state space.
Cricket analogy: Recalculating a batter's career average from scratch every time it's mentioned during commentary, instead of keeping a running cache, turns a simple lookup into a slow re-tally every single time.
Q: What are incorrect base cases in recursion, and what problems do they cause?
A base case that is missing, wrong, or reached too late causes infinite recursion (eventually a RecursionError / stack overflow), or a subtly wrong answer that only shows up on edge inputs like an empty list, a single element, or n=0. For example, a recursive factorial that checks 'if n == 1: return 1' will infinite-loop on n=0 because it never matches the base case as n decrements past 1 into negative numbers without a matching condition. The fix is to explicitly enumerate every base case the recursive structure implies (empty input, single element, boundary index) and test each one directly, not just the general recursive case.
Cricket analogy: A rain-delay protocol that never specifies what happens at zero overs remaining can leave the umpires stuck in an endless loop of recalculating a target that never resolves, like a missing base case.
Q: Why does Dijkstra's algorithm fail with negative edge weights, and what should you use instead?
Dijkstra assumes that once a node's shortest distance is finalized (popped from the priority queue with the minimum tentative distance), no future edge can improve it — that assumption only holds if all edges are non-negative. A negative edge encountered after a node is finalized can retroactively create a shorter path that Dijkstra will never revisit, producing a wrong answer without raising any error. Bellman-Ford correctly handles negative weights by relaxing every edge V-1 times and can additionally detect negative cycles, at the cost of higher time complexity, O(V * E) versus O((V+E) log V).
Cricket analogy: Once a team's net run rate is locked in after a match, a late scorecard correction discovered afterward, a negative adjustment, can retroactively change the standings without anyone noticing, just as Dijkstra can't revisit a finalized node.
Q: Does Python have integer overflow the way Java or C++ do, and why does that matter for algorithms?
No — Python integers have arbitrary precision and grow automatically as needed, so classic overflow bugs (like a 32-bit int wrapping to negative after multiplication) simply do not occur in pure Python. This is a real advantage when implementing algorithms like large-number Fibonacci, factorial, or combinatorics, but it can also hide a design mistake: an algorithm that is supposed to run in bounded integer space in Java/C++ might silently work in Python while being technically incorrect if ported to a language with fixed-width integers. In interviews, if asked about overflow, mention that Python sidesteps it but that you would still guard against unexpectedly huge intermediate values for performance reasons, since arbitrary-precision arithmetic on very large integers is not O(1).
Cricket analogy: A scorebook that never runs out of room to write a higher total, unlike a fixed-width scoreboard that wraps past 999, means huge totals are always recorded correctly, but writing a very long number still takes proportionally more ink and time.
Q: What is a common mutable-default-argument bug that breaks memoized recursive functions in Python?
Using a mutable default argument like def helper(n, memo={}) shares the same dictionary object across every call to the function, including calls from unrelated test cases or subsequent runs, which can leak stale cached values and produce wrong answers on the second invocation. The fix is to default the memo to None and initialize a fresh dictionary inside the function body (if memo is None: memo = {}), or pass the cache explicitly from the caller.
Cricket analogy: Reusing the same physical scorebook, a shared object, across two unrelated matches without resetting it between games leaks yesterday's scores into today's total, the same bug as a shared mutable default memo dict.
Q: Why do backtracking solutions sometimes run far slower than expected, and how do you fix it?
Backtracking without pruning explores the full combinatorial search space, and a missing or weak pruning condition (checking constraints too late, or not at all until a full candidate is built) causes wasted exploration of branches that were doomed from the start. In N-Queens, for example, checking for column/diagonal conflicts only after placing all N queens instead of after each placement turns a fast pruned search into a near-exhaustive one. The fix is to validate partial state as early as possible (prune immediately upon a constraint violation) rather than validating only complete candidates.
Cricket analogy: Waiting until the entire batting order is set before checking if two batters clash in the same role wastes far more effort than checking the conflict immediately after each selection, like weak pruning in N-Queens.
Q: What is a subtle mistake people make when reusing a visited set in graph traversal?
Marking a node as visited too late — for example, adding to the visited set when a node is dequeued in BFS instead of when it is enqueued — allows the same node to be added to the queue multiple times through different paths, wasting time and, in cyclic graphs, potentially causing incorrect results or infinite loops if visited tracking is skipped entirely. The fix is to mark a node visited at the moment it is discovered (enqueued/pushed), not when it is processed.
Cricket analogy: Marking a fielding position as covered only once a throw actually arrives there, instead of the moment it's called for, lets two fielders converge on the same ball simultaneously, wasting effort; mark it the moment it's called.
Quick Reference
- Greedy fails when a locally optimal choice can block a better global solution (e.g., 0/1 knapsack).
- Verify DP table index meaning (dp[i][j] = first i, first j characters, not raw indices) before trusting loop bounds.
- Missing memoization turns polynomial subproblem counts into exponential recomputation.
- Enumerate every base case implied by the recursive structure (empty, single element, boundary).
- Dijkstra requires non-negative weights; use Bellman-Ford when negative weights are possible.
- Python integers have arbitrary precision, so classic fixed-width overflow bugs do not occur.
- Never use a mutable default argument (like memo={}) for a memoization cache in Python.
- Prune backtracking search as early as possible, not only after a full candidate is built.
- Mark graph nodes visited at discovery time (enqueue/push), not at processing time.
- Always hand-trace a tiny example before trusting DP or recursive loop bounds.
Key Takeaways
- Prove (or disprove with a counterexample) the greedy-choice property before committing to a greedy algorithm.
- Write the exact meaning of each DP state before indexing into the table.
- Add memoization whenever a recursive solution has overlapping subproblems.
- Explicitly test every base case, not just the general recursive case.
- Match the algorithm to the constraints — negative weights rule out Dijkstra.
- In Python, watch for mutable default arguments corrupting caches across calls.
Practice what you learned
1. Why can a greedy algorithm fail on the 0/1 knapsack problem?
2. What is the most reliable way to prevent off-by-one errors in a DP table implementation?
3. What causes naive recursive Fibonacci to run in O(2^n) time?
4. Why is `def helper(n, memo={})` a dangerous pattern in Python for a memoized recursive function?
5. Why does Dijkstra's algorithm produce incorrect results on graphs with negative edge weights, and which algorithm should be used instead?
Was this page helpful?
You May Also Like
Common Algorithm Interview Questions
A curated set of frequently asked algorithm interview questions with clear, technically accurate answers.
Introduction to Dynamic Programming
Learn what dynamic programming is and the two conditions — overlapping subproblems and optimal substructure — that make it applicable.
Bellman-Ford Algorithm
A single-source shortest path algorithm that handles negative edge weights and detects negative-weight cycles.