📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

๐Ÿ“š Interview question

What is dynamic programming? Explain overlapping subproblems and optimal substructure, compare memoisation (top-down) with tabulation (bottom-up), and solve a problem such as minimum coins for an amount in C#.

Dynamic programming basics: recognising overlapping subproblems and optimal substructure, memoisation versus tabulation, and a worked minimum-coins example with space optimisation.

Mid-Level Coding Advanced algorithms dynamic-programming memoization optimization recursion

๐ŸŽฏ Key points

  • DP applies when a problem has overlapping subproblems (the same subproblem recurs) and optimal substructure (the best answer is built from best sub-answers)
  • Memoisation is top-down: the natural recursion plus a cache of computed states, so each state is solved once and only reachable states are computed
  • Tabulation is bottom-up: fill a table in dependency order with loops, with no recursion and no stack-depth risk
  • Recipe: define the state in one sentence, write the recurrence, set base cases, choose the iteration order, then read the answer
  • Complexity is the number of distinct states times the work per state; minimum coins is O(amount × coins) time and O(amount) space
  • Store the choice made at each state so you can reconstruct the actual solution, not only its value
  • Space can often drop to the previous row (rolling arrays), for example edit distance from O(n·m) to O(m)
  • Greedy is not DP: for coins {1, 3, 4} and amount 6 greedy gives 3 coins but DP finds 2; real uses include diff/LCS, Levenshtein, knapsack-style selection and scheduling

โฑ 30-second answer

Dynamic programming (DP) solves a problem by combining answers to smaller subproblems, storing each answer so it is computed once. It applies when:

  1. Overlapping subproblems: the same subproblem appears many times (naive Fibonacci recomputes fib(3) repeatedly).
  2. Optimal substructure: the best answer is built from best answers to subproblems.

Two styles: memoisation (recursion plus a cache, top-down) and tabulation (fill a table in order, bottom-up, no recursion). Minimum coins: dp[a] = 1 + min(dp[a - c]) over coins c, O(amount ร— coins) time.

๐Ÿ—ฃ Full answer (senior)

The idea

Many problems have a recursive definition that, written naively, recomputes the same thing exponentially many times. DP removes the repetition by remembering.

naive fib(5)
                fib(5)
              /        \
          fib(4)       fib(3)        <- fib(3) computed twice
          /    \        /   \
      fib(3) fib(2)  fib(2) fib(1)   <- fib(2) three times

Naive: O(1.618โฟ). With a cache: each fib(k) computed once, O(n).

The two conditions

  • Overlapping subproblems: without them, plain recursion or divide and conquer (merge sort) is enough.
  • Optimal substructure: the optimal solution contains optimal solutions to subproblems. Shortest paths have it; longest simple paths do not.

Memoisation versus tabulation

Memoisation (top-down) Tabulation (bottom-up)
Shape recursive function + Dictionary/array cache loop filling an array in dependency order
Computes only states actually reached usually every state
Stack risk yes, depth can overflow none
Space optimisation hard easy (keep only the last row)
Easier to write often, follows the recurrence needs the right iteration order

The recipe

  1. Define the state: dp[a] = fewest coins to make amount a.
  2. Recurrence: dp[a] = 1 + min(dp[a - c]) for each coin c โ‰ค a.
  3. Base case: dp[0] = 0.
  4. Order: a from 1 to amount (each depends on smaller amounts).
  5. Answer: dp[amount], or "impossible" if still infinite.

Complexity: O(amount ร— coins) time, O(amount) space.

Where DP shows up in real work

  • Text diff (git, code review tools, test snapshot diffs): longest common subsequence / edit distance.
  • Fuzzy matching: Levenshtein distance for "did you mean" search or deduplicating customer names.
  • Pricing and bundling: knapsack-style "best combination within budget".
  • Scheduling: weighted interval scheduling (maximise value of non-overlapping bookings).
  • Query optimisers use DP to pick join orders.

Production example

A promotions engine chose which vouchers to apply to a basket by trying every subset: O(2โฟ). With 25 eligible vouchers it took seconds and timed out at checkout. The constraint was a cap on total discount per basket, so it was a 0/1 knapsack. A DP over (voucher index, discount in cents up to the cap) made it O(n ร— cap), a few milliseconds, with the same results as brute force in a property-based test.

๐Ÿ” What the interviewer is checking

Checks whether the candidate can recognise when DP applies and build a correct solution.

Green flags

  • Explains overlapping subproblems and optimal substructure with an example.
  • Defines the state precisely, writes the recurrence and base case.
  • Compares memoisation and tabulation, including stack depth.
  • States time and space as states ร— work per state, and optimises space.
  • Reconstructs the solution, not just the number.
  • Links DP to real uses (diff, Levenshtein, knapsack).

Red flags

  • Uses greedy and calls it DP.
  • Cannot explain why naive recursion is exponential.
  • Off-by-one errors in table size or base case.
  • Thinks DP means "any recursion with a cache" without the conditions.

๐Ÿ“š Learn in depth

1. Spotting a DP problem

Ask three questions:

  1. Can I write the answer as a recurrence on smaller inputs?
  2. Do the same smaller inputs repeat? (draw the recursion tree)
  3. Is the number of distinct states small (polynomial)?

If yes to all three, DP turns exponential into polynomial: time = number of states ร— work per state.

2. Minimum coins, worked through

Coins {1, 3, 4}, amount 6.

a     : 0  1  2  3  4  5  6
dp[a] : 0  1  2  1  1  2  2
                        ^ dp[6] = 1 + min(dp[5], dp[3], dp[2]) = 1 + min(2, 1, 2) = 2  (3 + 3)

Greedy (largest coin first) gives 4 + 1 + 1 = 3 coins, which is wrong. DP considers every last coin and keeps the best.

3. Reconstructing the choice

Store which coin achieved the minimum, then walk back:

var choice = new int[amount + 1];
// inside the loop when you improve dp[a]: choice[a] = c;
var used = new List<int>();
for (int a = amount; a > 0; a -= choice[a]) used.Add(choice[a]);

Interviewers often follow up with "which coins?", and production code almost always needs the plan, not just the number.

4. Two-dimensional DP: edit distance

dp[i, j] = edits to turn the first i chars of s into the first j chars of t.

        ""  k  i  t  t  e  n
  ""     0  1  2  3  4  5  6
  s      1  1  2  3  4  5  6
  i      2  2  1  2  3  4  5
  t      3  3  2  1  2  3  4
  t      4  4  3  2  1  2  3
  i      5  5  4  3  2  2  3
  n      6  6  5  4  3  3  2
  g      7  7  6  5  4  4  3   <- distance("sitting", "kitten") = 3

dp[i,j] = s[i-1]==t[j-1] ? dp[i-1,j-1]
        : 1 + min(dp[i-1,j]   (delete),
                  dp[i,j-1]   (insert),
                  dp[i-1,j-1]) (replace)

O(n ยท m) time. Each row only needs the previous row, so space drops from O(n ยท m) to O(m).

5. Memoisation pitfalls in C#

  • Stack depth: memoised recursion on n = 100,000 still recurses 100,000 deep. Use tabulation.
  • Cache key: for multi-parameter states use a value tuple key (int i, int j), or better, a 2D array when the ranges are known (much faster than a dictionary).
  • Shared caches: a static Dictionary memo shared across requests is not thread-safe and grows forever. Keep the memo local to one computation.

6. Classic DP patterns

Pattern Example State
1D linear climbing stairs, house robber, min coins dp[i]
Two strings LCS, edit distance, diff dp[i, j]
Knapsack budget, vouchers, capacity dp[item, capacity]
Intervals weighted scheduling, matrix chain dp[i] with binary search for previous compatible
Grid paths unique paths, min path sum dp[r, c]

7. Try it yourself

// Compare naive vs memoised vs tabulated Fibonacci
using System.Diagnostics;

static long Naive(int n) => n < 2 ? n : Naive(n - 1) + Naive(n - 2);

static long Memo(int n, long[] memo)
{
    if (n < 2) return n;
    if (memo[n] != 0) return memo[n];
    return memo[n] = Memo(n - 1, memo) + Memo(n - 2, memo);
}

static long Table(int n)
{
    long prev = 0, cur = 1;
    for (int i = 2; i <= n; i++) (prev, cur) = (cur, prev + cur);
    return n == 0 ? 0 : cur;
}

var sw = Stopwatch.StartNew();
Console.WriteLine($"{Naive(40)} naive {sw.ElapsedMilliseconds} ms");
sw.Restart();
Console.WriteLine($"{Memo(40, new long[41])} memo {sw.Elapsed.TotalMilliseconds:F3} ms");
sw.Restart();
Console.WriteLine($"{Table(90)} table(90) {sw.Elapsed.TotalMilliseconds:F3} ms");

Naive(40) makes about 330 million calls; the others make 40.

๐Ÿ’ป Code

// Minimum coins: tabulation, O(amount * coins) time, O(amount) space
public static (int Count, List<int> Coins)? MinCoins(int[] coins, int amount)
{
    const int Inf = int.MaxValue;
    var dp = new int[amount + 1];
    var choice = new int[amount + 1];
    Array.Fill(dp, Inf);
    dp[0] = 0;

    for (int a = 1; a <= amount; a++)
        foreach (var c in coins)
            if (c <= a && dp[a - c] != Inf && dp[a - c] + 1 < dp[a])
            {
                dp[a] = dp[a - c] + 1;
                choice[a] = c;
            }

    if (dp[amount] == Inf) return null;
    var used = new List<int>();
    for (int a = amount; a > 0; a -= choice[a]) used.Add(choice[a]);
    return (dp[amount], used);
}

// Memoised (top-down) version of the same recurrence
public static int MinCoinsMemo(int[] coins, int amount)
{
    var memo = new int?[amount + 1];
    int Solve(int a)
    {
        if (a == 0) return 0;
        if (memo[a] is int cached) return cached;
        int best = int.MaxValue;
        foreach (var c in coins)
            if (c <= a && Solve(a - c) is var sub && sub != int.MaxValue)
                best = Math.Min(best, sub + 1);
        return (memo[a] = best).Value;
    }
    return Solve(amount);   // recursion depth up to amount: prefer tabulation for big amounts
}

// Edit distance with O(m) space (two rows)
public static int Levenshtein(ReadOnlySpan<char> s, ReadOnlySpan<char> t)
{
    var prev = new int[t.Length + 1];
    var cur = new int[t.Length + 1];
    for (int j = 0; j <= t.Length; j++) prev[j] = j;
    for (int i = 1; i <= s.Length; i++)
    {
        cur[0] = i;
        for (int j = 1; j <= t.Length; j++)
            cur[j] = s[i - 1] == t[j - 1]
                ? prev[j - 1]
                : 1 + Math.Min(Math.Min(prev[j], cur[j - 1]), prev[j - 1]);
        (prev, cur) = (cur, prev);
    }
    return prev[t.Length];
}

โš  Common mistakes

  • Mistake: Using greedy where DP is needed, for example coins {1, 3, 4} for amount 6. Why it is wrong: Taking the largest coin first gives 4 + 1 + 1 = 3 coins, but 3 + 3 = 2 coins is optimal. Greedy commits early and never reconsiders. Instead: Use DP unless you can prove the greedy choice is always safe (an exchange argument).

  • Mistake: Defining the state vaguely. Why it is wrong: If dp[i] has no precise meaning, the recurrence and base cases come out wrong. Instead: Write the definition in one sentence first, such as "dp[a] is the fewest coins that sum to exactly a".

  • Mistake: Memoising with deep recursion on large inputs. Why it is wrong: Recursion depth can equal the input size, and a stack overflow in .NET cannot be caught; it ends the process. C# does not guarantee tail-call optimisation to save you. Instead: Use bottom-up tabulation for large state spaces, or keep memoisation only when depth is known to be small.

  • Mistake: Caching in a Dictionary<string, int> with string-concatenated keys. Why it is wrong: Each lookup allocates and hashes a string, which is far slower than array indexing. Instead: Use arrays for dense integer states, or value-tuple keys such as (int I, int J) when states are sparse.

  • Mistake: Sharing a static memo across requests. Why it is wrong: A plain Dictionary<TKey,TValue> is not thread-safe, and the cache grows without limit. Instead: Keep the memo local to one computation.

  • Mistake: Returning only the optimal value. Why it is wrong: Real features need the plan: which vouchers, which edits, which bookings. Instead: Store the choice per state and reconstruct by walking back from the answer.

  • Mistake: Keeping the full table when only the previous row is needed. Why it is wrong: O(n ยท m) memory can reach gigabytes for long strings. Instead: Keep two rows (O(m) space) when you do not need full reconstruction.

๐Ÿ› Senior insight: trade-offs

A senior engineer recognises DP in disguise. The interview problem is minimum coins; the production problem is "checkout times out once 25 vouchers are eligible". The skill is mapping the business rule to a state and a recurrence, then estimating the state count (for example n ร— discount cap) to check it is feasible before writing code.

Question Memoisation (top-down) Tabulation (bottom-up)
Easier to write from the recurrence Yes Needs the right iteration order
Computes only reachable states Yes, good for sparse state spaces Usually fills every state
Stack overflow risk Yes, depth can match input size None
Space optimisation (rolling rows) Hard Easy
Typical production choice Small, sparse or irregular state spaces Large, dense state spaces on hot paths

Seniors also validate DP against brute force on small inputs with property-based tests (FsCheck or CsCheck), because DP bugs are usually off-by-one errors in base cases or iteration order and produce plausible but wrong answers. And they know when not to bother: if n is tiny and fixed, brute force is simpler and obviously correct; if the state space is too big, they use heuristics or approximations and document the trade-off.

๐Ÿ”— Follow-ups the interviewer may ask

  1. When: Candidate shows greedy fails for coins {1, 3, 4} Why: Checks when greedy is safe and how to prove it
    How to answer

    A greedy algorithm builds a solution step by step, always taking the locally best choice and never revisiting it. It is fast and simple, but only optimal when the problem has the greedy-choice property (a locally best choice is part of some optimal solution) and optimal substructure.

    • Optimal: interval scheduling by earliest end time, Dijkstra (non-negative weights), Huffman coding, Kruskal/Prim, fractional knapsack.
    • Not optimal: coins {1, 3, 4} for 6, 0/1 knapsack, travelling salesman.

    In production, greedy is often a good-enough heuristic (bin packing, load balancing) as long as you know it is approximate.

  2. When: Candidate writes a memoised recursive solution Why: Checks stack depth risk and converting to iteration
    How to answer

    Recursion is a function solving a problem by calling itself on a smaller version of it. Every recursive function needs:

    1. A base case that stops.
    2. A recursive case that moves towards the base case.

    Each call pushes a frame on the thread's call stack (about 1 MB by default on Windows). Too deep and you get a StackOverflowException, which cannot be caught and kills the process. Recursion fits naturally recursive data (trees, nested JSON, folders), but use a loop with an explicit Stack<T> or Queue<T> when depth can be large or is controlled by input.

  3. When: Candidate mentions the DP table size Why: Checks space analysis and rolling-array optimisation
    How to answer

    Space complexity is how the extra memory an algorithm needs grows with input size. Usually we quote auxiliary space, not counting the input.

    • Reversing an array in place: O(1).
    • Copying to a new list, or a HashSet of seen items: O(n).
    • Recursion uses stack: depth d costs O(d) (merge sort O(log n) stack plus O(n) buffer).

    In .NET, space also means allocations and GC pressure: an O(n) algorithm that allocates per item can be slower than one that reuses a buffer. Common trade: spend O(n) memory (a dictionary, memo table) to save time.

๐Ÿ›  Real-world problems: how you handle them

Voucher selection timing out at checkout
Situation
An e-commerce checkout chose the best combination of vouchers by trying every subset. Marketing launched a campaign that made [25] vouchers eligible for some baskets.
Signals
Checkout p99 latency jumped from [300 ms] to over [8 s] for affected baskets; request timeouts and abandoned carts rose; CPU on the pricing service pinned at 100%.
Investigation
A dotnet-trace flame graph showed almost all time in the recursive subset enumeration. Counting calls confirmed 2^25 (about 33 million) combinations per basket. The rules showed it was a 0/1 knapsack: maximise discount subject to a per-basket discount cap.
Resolution
Replaced brute force with a DP over (voucher index, discount in cents up to the cap), O(n x cap), with choice reconstruction. Short term, capped eligible vouchers at [10] while the fix shipped.
Prevention
Property-based tests comparing DP with brute force for n up to 15; a performance test with 50 vouchers in CI; an alert on pricing p99 latency.

How to tell it (STAR)

In my role at [company], our checkout started timing out after a marketing campaign. I profiled it and found the voucher engine tried every subset, which is 2 to the power n, and n had grown to [25]. I recognised it as a knapsack problem with the discount cap as capacity, rewrote it as a bottom-up DP and kept brute force as a test oracle. Latency went from [8 seconds] back to [under 5 ms] for that step, and the campaign stayed live.

โ†ฉ Usually asked after

More in Algorithms

Last updated 23 Oct 2026.

๐ŸŒ Sangam's Webring

โช Previous ๐Ÿ  Main Next โฉ
๐Ÿ’ฌ Chat with me