CMD Guide
HomeDSADynamic Programming

Introduction

Dynamic Programming (DP) solves a problem by breaking it into overlapping subproblems, solving each subproblem exactly once, and reusing the stored answer instead of recomputing it — turning an exponential tree of repeated recursive calls into a linear or polynomial table of unique states.

Recognize the pattern

Running example: 0/1 Knapsack

Given item weights/values and a capacity, choose a subset of items (each item taken 0 or 1 times) to maximize total value without exceeding capacity. At each item you make one binary decision: skip it, or take it (if it fits) and recurse on the remaining capacity.

Brute force (recursion on all subsets)

int knapsackRec(int i, int cap, int[] wt, int[] val) {
    if (i == wt.length || cap == 0) return 0;
    int skip = knapsackRec(i + 1, cap, wt, val);
    int take = 0;
    if (wt[i] <= cap) {
        take = val[i] + knapsackRec(i + 1, cap - wt[i], wt, val);
    }
    return Math.max(skip, take);
}

Cost: at every index there are 2 branches, so the call tree has up to 2^n leaves — O(2^n) time, O(n) space (recursion stack). Most of those branches recompute the identical (index, capacity) pair.

Optimal: memoize, then tabulate

Cache each unique (i, cap) state the first time it's solved; every later call to the same state is O(1). That collapses the exponential tree down to the number of distinct states.

int knapsackMemo(int i, int cap, int[] wt, int[] val, Integer[][] memo) {
    if (i == wt.length || cap == 0) return 0;
    if (memo[i][cap] != null) return memo[i][cap];
    int skip = knapsackMemo(i + 1, cap, wt, val, memo);
    int take = 0;
    if (wt[i] <= cap) {
        take = val[i] + knapsackMemo(i + 1, cap - wt[i], wt, val, memo);
    }
    return memo[i][cap] = Math.max(skip, take);
}

// Bottom-up, same recurrence, no recursion:
int knapsackTab(int[] wt, int[] val, int capacity) {
    int n = wt.length;
    int[][] dp = new int[n + 1][capacity + 1];
    for (int i = n - 1; i >= 0; i--) {
        for (int cap = 0; cap <= capacity; cap++) {
            int skip = dp[i + 1][cap];
            int take = (wt[i] <= cap) ? val[i] + dp[i + 1][cap - wt[i]] : 0;
            dp[i][cap] = Math.max(skip, take);
        }
    }
    return dp[0][capacity];
}

Complexity, derived

The recursion is defined by T(i, cap) = T(i+1, cap) + T(i+1, cap-wt[i]). Without memoization this recurrence unrolls into a binary tree of depth n, giving T(n) = 2·T(n-1) → O(2^n) calls. With memoization, the number of distinct states is bounded by the grid of (index, capacity) pairs: n choices of index × (capacity+1) choices of remaining capacity = O(n·capacity) states, each solved in O(1) work once cached → O(n·capacity) time. Space is O(n·capacity) for the table (recursion adds O(n) stack for the memoized version); the tabulated version can be compressed to O(capacity) by keeping only the current and next row, since row i only depends on row i+1.

Traced example

Items, 0-indexed as (weight, value): item0=(1,1), item1=(3,4), item2=(4,5), item3=(5,7); capacity = 7. dp[i][cap] means "best value achievable using items i..3 within capacity cap", so row 4 is the empty-suffix base case and row 0 (the final answer) is filled last, from the bottom up.

i (item)cap=0347
4: none left0000
3: item30007
2: item2, item30057
1: item1, item2, item30459
0: item0, item1, item2, item30459

Two cells worth checking by hand, since row 0 is where mistakes hide: dp[0][3] = max(skip = dp[1][3] = 4, take = val[0] + dp[1][3-1] = 1 + dp[1][2]). dp[1][2] is 0 (item1 alone doesn't fit in capacity 2), so take = 1, and dp[0][3] = max(4, 1) = 4. Likewise dp[0][4] = max(skip = dp[1][4] = 5, take = 1 + dp[1][3] = 1 + 4 = 5) = 5. Both match the recurrence exactly — row 0 is (0, 4, 5, 9).

Reading dp[0][7] = 9: it comes from the skip branch at item0 — dp[0][7] = dp[1][7] = 9, meaning item0 is left out entirely. One level down, dp[1][7] = 9 comes from the take branch at item1 — val[1] + dp[2][7-3] = 4 + dp[2][4] = 4 + 5 = 9. And dp[2][4] = 5 comes from the take branch at item2 — val[2] + dp[3][0] = 5 + 0 = 5. Walking those choices back up gives the concrete optimal set: item1(3,4) + item2(4,5) = weight 7, value 9 — an exact fit at capacity 7, and the true optimum (any set including item0 or item3 either exceeds 7 or scores lower, e.g. item0+item1+item2 has weight 8 and doesn't fit).

Pitfalls

When to use / when not

Use DP when the problem has optimal substructure + overlapping subproblems and the state space (here, index × capacity) is small enough to enumerate — polynomial, not exponential, in the input size. Avoid it when: (a) a greedy choice is provably optimal (e.g., fractional knapsack — sort by value/weight ratio, no need for a table, O(n log n) beats O(n·capacity)); (b) capacity or another state dimension is astronomically large (pseudo-polynomial O(n·capacity) blows up when capacity ~10^9), in which case branch-and-bound or approximation algorithms are preferred — and if instead the item count is tiny (n ≤ ~40) while capacity is astronomical, meet-in-the-middle (split the items in half, enumerate all 2^(n/2) subset sums of each half, then two-pointer/binary-search to combine) runs in O(2^(n/2)) and sidesteps the capacity dimension entirely; (c) subproblems don't actually overlap, in which case plain divide-and-conquer is simpler and needs no memo table.

Takeaways

Recall: Why does the 1-D space-optimized 0/1 knapsack array require iterating the capacity dimension from high to low, and what breaks if you go low to high?


Synthesized from the classic 0/1 Knapsack DP pattern; complexity and transition derivations are original to this page.

🤖 Don't fully get this? Learn it with Claude

Stuck on Introduction? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Introduction** (DSA) and want to truly understand it. Explain Introduction from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Introduction** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Introduction** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Introduction** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes