Chapter 11: Dynamic Programming Advanced
Tackle advanced DP problems involving two sequences, item selection, and subset partitioning. Master LCS, edit distance, LIS, knapsack variants, and partition problems that form the backbone of competitive programming and technical interviews.
- Topics
- 6
- Demos
- 3
- Videos
- 3
- Min read
- 30
Chapter Overview
Building on the DP fundamentals from the previous chapter, we now tackle problems that require more sophisticated state definitions and recurrences. These problems typically involve two-dimensional DP tables, multi-choice decisions at each step, and clever reductions from one problem to another.
The key themes in advanced DP are:
- Dual-sequence DP: Problems like LCS and edit distance operate on two strings or sequences simultaneously, requiring a 2D table indexed by positions in both sequences
- Item selection DP: Knapsack problems involve choosing from a set of items subject to a constraint, with each item either included or excluded (0/1) or used unlimited times (unbounded)
- Subsequence DP: LIS requires finding optimal subsequences within a single sequence, with an elegant O(n log n) optimization using binary search
- Reduction to known problems: Many problems (like partition equal subset sum) can be reformulated as variants of simpler known problems (subset sum, which is itself a knapsack variant)
This chapter covers:
- Longest Common Subsequence: 2D DP on two sequences with subsequence matching
- Edit Distance: Minimum operations to transform one string into another
- Longest Increasing Subsequence: Classic 1D subsequence problem with an O(n log n) optimization
- 0/1 Knapsack: The foundational item selection problem
- Unbounded Knapsack & Coin Change: Variants with unlimited item usage
- Partition Equal Subset Sum: Reducing a partition problem to subset sum DP
Chapter Roadmap
Click any topic to jump in
Longest Common Subsequence
$O(mn)$ 2D DP on two strings with reconstruction via backward walk.
Edit Distance
Insert, delete, replace as three transitions; $dp[i][j] = 1 + \min(\cdot)$.
Longest Increasing Subsequence
$O(n^2)$ DP vs. the patience-sort $O(n \log n)$ tails array.
Each tackles a different aspect
0/1 Knapsack
Binary include/exclude with $O(nW)$ DP and 1D $O(W)$ space via reverse iteration.
Unbounded Knapsack & Coin Change
Forward iteration allows item reuse — and solves minimum coin change.
Partition Equal Subset Sum
Reduction to subset sum $S/2$ with 1D boolean DP and bitset speedup.
Topics
6 topics in this chapter
When you run a diff on two versions of a file, the tool must decide which lines stayed the same and which were added or removed. Matching lines greedily, pairing each line with the first equal line in the other file, can lock in an early match that blocks many later ones. The real question is: what is the longest sequence of lines that appears in both files, in the same order, with gaps allowed? That is the longest common subsequence, and it also underlies DNA comparison and plagiarism checks.
The previous chapter ended with Maximal Square, where each cell of a 2D table described a region of one grid. This chapter keeps the 2D table but changes what the axes mean: rows index a prefix of the first sequence and columns index a prefix of the second. Each cell compares only the last characters of the two prefixes, and the answer grows one character at a time.
We first build the table and its recurrence on a small pair of strings. Then we walk back through the finished table to recover an actual common subsequence, not just its length.
Definition
Given sequences of length and of length , a common subsequence is a sequence obtainable from both by deleting elements without reordering. The LCS problem asks for the longest one. With the LCS length of and , it is solved in time, and one LCS is recovered in extra time.
In this topic
- 12D Table Construction
- 2Reconstructing the LCS
2D Table Construction
Trying every subsequence of one string costs checks, so we need a state that reuses work. Let be the LCS length of the first characters of and the first characters of . An empty prefix shares nothing, so row 0 and column 0 are zero. If the last characters match, , some LCS ends with that pair, giving . If not, at least one of the two characters is unused, so drop either and keep the better result: . Row-major filling satisfies every dependency, as in the grid DP of the last chapter.
Define = length of LCS of prefixes and . Recurrence: if , else . Time , space reducible to . Correctness: any LCS either ends with the matching pair (use diagonal) or skips a character from one string (use one of the two neighbors).
Find the LCS of 'ABCBDAB' and 'BDCAB'.
Reconstructing the LCS
The table stores lengths, but a diff tool needs the matched characters themselves. Start at and retrace the decision that produced each value. If , that pair belongs to an LCS: record it and step diagonally to . Otherwise step to whichever neighbour, or , holds the larger value, since that is where the length came from. Stop at row 0 or column 0 and reverse the recorded characters. The walk takes steps and needs no extra pointers. When the two neighbours tie, either choice is valid, which is why one pair of strings can have several different LCSs.
After filling the DP table, walk backward from : if , append that character and move to ; otherwise move to whichever of or has the larger value. This reconstructs one LCS in time after the DP. Storing parent pointers is optional since the DP values themselves encode the path.
Given the DP table for 'ACE' and 'ABCDE', trace back to find the LCS.
Related Problems on PixelBank
A user types "recieve" and the spell checker must suggest "receive". To rank candidates it needs a measure of how far apart two words are, and counting mismatched positions fails once a letter is inserted or deleted, because later positions shift. "cat" and "at" differ in all three positions, yet a single deletion turns one into the other. The right measure is the fewest single-character edits, and computing it means choosing how to align the two strings.
The previous topic, Longest Common Subsequence, used a 2D table over prefixes of two strings and maximised the number of matched characters. Edit distance uses the same table with a different question: it minimises the number of operations. Each cell considers three moves, delete a character from the first string, insert one from the second, or replace one with the other, plus a free diagonal move when the characters already match. Base cases are no longer zero, because turning a prefix into an empty string costs one deletion per character.
We first derive the three-way recurrence and check it on a classic pair. Then we read the filled table as a map of alignments and trace the cheapest route through it.
Definition
The edit (Levenshtein) distance between strings and is the minimum number of single-character insertions, deletions, and substitutions that transform into . With , , and on a match, otherwise of the three neighbours, it is computed in time. Under the Strong Exponential Time Hypothesis, no algorithm is substantially faster on general strings.
In this topic
- 1Three Operations as Transitions
- 2Visualizing the DP Table
Three Operations as Transitions
Let be the fewest edits turning the first characters of word1 into the first characters of word2. Look at how an optimal edit script treats the last characters. If they are equal, nothing is needed and . Otherwise the script either replaces word1[i-1] with word2[j-1], costing ; deletes word1[i-1], costing ; or inserts word2[j-1] at the end, costing . Take the minimum. The base cases and count pure deletions and insertions. Setting them to zero, copied from LCS, makes every answer too small.
Define = min operations to turn into . If : . Otherwise: for delete, insert, and replace respectively. Each transition is and the state space is , giving time — matching the best known bound for general strings.
What is the edit distance between 'horse' and 'ros'?
Visualizing the DP Table
The filled table is a map of every alignment. The first row counts up to , inserting all of word2, and the first column counts up to , deleting all of word1. Each interior cell reads three neighbours: diagonal for a match or replacement, above for a deletion, and left for an insertion. So the table is a shortest-path grid in which horizontal and vertical steps cost 1 and a diagonal step costs 0 or 1. Following the minimum from back to spells out an optimal edit script. Because each row reads only the one above, two rows of length are enough when only the distance is needed.
The edit-distance DP table can be read as a shortest-path search on an lattice where diagonal edges cost 0 (match) or 1 (replace), horizontal edges cost 1 (insert), vertical edges cost 1 (delete). The minimum-cost path from to is the edit distance, which is why values grow monotonically outward from the origin.
Build the DP table for 'cat' and 'cut'.
Related Problems on PixelBank
A stock chart shows a closing price for each day, and an analyst wants the longest run of days, not necessarily consecutive, on which the price rose each time. The same question appears when stacking boxes so each is larger than the one below, and inside diff tools that must decide which unique lines kept their order. Checking every subsequence of a sequence of values means candidates, hopeless beyond a few dozen days.
The previous topic, Edit Distance, compared two strings with a 2D table. The Longest Increasing Subsequence has only one sequence, so the state is one-dimensional: is the longest increasing subsequence that ends exactly at position . Fixing the ending element is the key step, because it tells us which earlier elements can come before it, those that are smaller. Filling this table checks every earlier element for every position, which costs .
We first build that quadratic DP and see why the answer is the maximum over all entries, not the last one. Then we replace the inner scan with binary search over a sorted array of tails, which cuts the time to and links DP to patience sorting.
Definition
A strictly increasing subsequence of is a choice of indices with . The LIS problem asks for the largest . The DP runs in , and the tails method with binary search runs in . The subsequence need not be contiguous, and the answer is the maximum over all ending positions.
In this topic
- 1O(n^2) DP Approach
- 2O(n log n) with Binary Search
O(n^2) DP Approach
Let be the length of the longest strictly increasing subsequence that ends at index . Ending there, the subsequence either consists of alone, length 1, or extends one ending at some earlier with . So , or 1 if no such exists. Filling left to right works because each entry reads only earlier ones. The inner scan over all gives time. The LIS can end anywhere, so the answer is . Returning is a common bug, and recovering the subsequence itself needs a parent index for each entry.
Define = length of LIS ending at index . Recurrence: , with if no valid . Total time is . The answer is . Straightforward but quadratic, and reconstruction requires storing parent pointers.
Find the LIS of [3, 1, 4, 1, 5, 9, 2, 6].
O(n log n) with Binary Search
The quadratic scan asks, for each element, which earlier subsequences it can extend. Keep less information: let tails[k] be the smallest value that ends any increasing subsequence of length seen so far. A smaller tail is always better, since more future values can extend it. tails is strictly increasing, so for each new value binary-search the first entry that is at least . If none exists, append and the LIS grows by one; otherwise overwrite that entry with . Each step is , giving . The length of tails is the answer, but tails itself is generally not a valid subsequence.
Maintain an array where = smallest possible tail of an increasing subsequence of length seen so far. For each new element, binary-search its insertion point in — if it replaces a value, the existing subsequence tail becomes smaller (monotonically better); if it appends, the LIS grows by one. Total time is . Note: is not itself an LIS, only its length is correct.
Trace the tails array for [10, 9, 2, 5, 3, 7, 101, 18].
Related Problems on PixelBank
A hiker can carry 8 kilograms and has four items, each with a weight and a usefulness score. Which subset gives the most usefulness without going over the limit? The same decision appears when a cloud scheduler packs jobs onto a machine with fixed memory, or when an LLM application chooses which documents fit into a token budget. With items there are subsets, and the natural greedy rule, taking items in order of value per kilogram, can leave capacity unused and miss the best subset.
The previous topic, Longest Increasing Subsequence, decided which elements of a sequence to keep under an ordering rule. The 0/1 knapsack decides which items to keep under a capacity rule, and the state therefore needs a second index for remaining capacity. For each item the choice is binary, which gives the problem its name: take it once, if it fits, or leave it. This is the include-or-exclude pattern from House Robber, with capacity playing the role of the adjacency constraint.
We first build the two-dimensional table over items and capacities. Then we compress it to one array and see why the capacity loop must run backwards to keep each item single-use.
Definition
Given items with weights and values and a capacity , the 0/1 knapsack problem chooses a subset with total weight at most and maximum total value, using each item at most once. With , it runs in time, which is pseudo-polynomial, and in space.
In this topic
- 1Include or Exclude Decision
- 2Space Optimization with 1D Array
Include or Exclude Decision
Let be the best value obtainable from the first items with capacity , where item has weight and value . An optimal packing either leaves item out, scoring , or puts it in, which uses of the capacity and scores . The second branch is allowed only when . Row in both branches is what stops item from being used twice. With no items the value is zero, so row 0 is all zeros. The table has cells at each, which is polynomial in but exponential in the bits needed to write .
Define = max value using first items with capacity . Recurrence: if , else . Time , space . This is pseudo-polynomial: can be exponentially larger than in input bits, which is why 0/1 knapsack is NP-hard despite having a polynomial-in- algorithm.
Items: [(weight=2, value=3), (weight=3, value=4), (weight=4, value=5), (weight=5, value=6)]. Capacity = 8.
Space Optimization with 1D Array
Row reads only row , so the whole table can collapse into one array dp of length . Before item is processed, dp[c] holds the previous row; the update is dp[c] = max(dp[c], dp[c - w_i] + v_i). The order of the capacity loop now matters. Running from down to means dp[c - w_i] is read before this pass has overwritten it, so it still describes packings without item . Running upward reads a value this pass has already updated, one that may already contain item , so the item can be taken again. That silent change turns the code into unbounded knapsack. Space drops from to .
Since only depends on , we can use a single 1D array. Key trick: iterate from down to (reverse order) so still refers to the old (previous-item) value — iterating forward would let us reuse the same item, which is the unbounded knapsack. Space drops from to with identical time.
Why does iterating right-to-left prevent using an item twice?
Related Problems on PixelBank
A vending machine must return 6 cents using coins worth 1, 3, and 4, as few coins as possible, with unlimited coins of each type. The cashier's habit of always taking the largest coin that fits gives 4 + 1 + 1, three coins, while 3 + 3 uses two. Greedy is optimal for many real coin systems but not all, so a machine handling any denominations needs a method that is always correct.
The previous topic, 0/1 Knapsack, allowed each item at most once and enforced that by running the capacity loop backwards. Coin change is an unbounded knapsack: every coin type can be used any number of times. The recurrence has the same shape, but running the capacity loop forwards is now exactly what we want, because a value computed earlier in the same pass may already contain the current item, and building on it reuses that item. Only the loop direction and the objective change.
We first compare unbounded and 0/1 knapsack on the same items to see where reuse helps. Then we specialise to coin change, minimising a count instead of maximising a value, and handle amounts that cannot be made at all.
Definition
Unbounded knapsack chooses non-negative multiplicities of items with weights and values to maximise value within capacity , via . Coin change minimises the number of coins summing to an amount : , , or infinity if unreachable. Both run in time and space.
In this topic
- 1Unbounded vs 0/1 Knapsack
- 2Coin Change: Minimum Coins
Unbounded vs 0/1 Knapsack
With unlimited copies, the state no longer needs to remember which items have been used, only how much capacity remains. Let be the best value within capacity . The last item placed can be any item with , and what remains is again an unbounded problem of capacity , so . In the one-array code this is the 0/1 update with the capacity loop running forwards, from up to : dp[c - w_i] may already include item , which is now allowed. Using the forward loop by accident on a 0/1 problem gives answers that are too large.
Unbounded knapsack allows unlimited copies of each item. The 1D recurrence iterated forward (from to ) intentionally lets the same item be reused, because may already include item . The single character flip — forward vs. reverse iteration — is what distinguishes the two variants.
Items: [(weight=1, value=1), (weight=3, value=4), (weight=4, value=5)]. Capacity = 7.
Coin Change: Minimum Coins
Coin change swaps the objective: each coin has value 1 in the count, and we minimise. Let be the fewest coins summing exactly to , with and every other entry starting at infinity to mean unreachable. For each amount, try every coin as the last coin: . Filling amounts upward lets each coin be reused. If is still infinity, no combination works and the answer is . Initialising with zero instead of infinity makes impossible amounts look free. Greedy is correct only for special coin systems, so the DP is the safe default.
Define = minimum coins to make amount (or if impossible). Recurrence: , with . Time , space . Greedy fails in general — for coins and target 6, greedy picks but optimal is .
Coins = [1, 3, 4]. Amount = 6.
Related Problems on PixelBank
Two teammates split a batch of jobs with known runtimes and want to finish at the same moment, which means dividing the runtimes into two groups with equal totals. Trying every assignment costs checks. There is also no simple greedy rule: giving each job to the less-loaded worker leaves [3, 3, 2, 2, 2] split as 5 and 7, even though 3 + 3 and 2 + 2 + 2 balance perfectly.
The previous topic, Unbounded Knapsack and Coin Change, reused items freely. Partition is the 0/1 case again, and the chapter's last idea is that it needs no new DP at all, only a reduction. If the total is odd, no split exists. If it is even, the two halves each sum to , so the question becomes whether some subset reaches exactly . That is subset sum, a 0/1 knapsack in which we record whether a total is reachable instead of maximising a value. The backwards loop from 0/1 Knapsack carries over unchanged.
We first carry out the reduction and fill a boolean reachability array on a small input. Then we speed it up with early exits and a bitset that updates every sum in one shift.
Definition
Partition Equal Subset Sum asks whether positive integers with total can be split into two subsets of equal sum. It holds exactly when is even and some subset sums to . A boolean DP, with descending, decides this in time and space.
In this topic
- 1Reduction to Subset Sum
- 2Optimization and Early Termination
Reduction to Subset Sum
A reduction answers a new problem by translating it into one we can already solve. If a partition exists, each side sums to ; conversely, a subset summing to leaves a complement with the same sum. So first reject odd , then ask whether target is reachable. Let be true when some subset of the numbers seen so far sums to , starting from true for the empty subset. For each number , loop from target down to and set . Descending order keeps each number single-use, exactly as in 0/1 knapsack.
Equal partition exists iff total sum is even and there is a subset summing to . This reduces to subset sum, which has a DP: . The pseudo-polynomial complexity is unavoidable since subset sum is NP-complete, but for typical array sums this works well.
Can [1, 5, 11, 5] be partitioned into two equal-sum subsets?
Optimization and Early Termination
The table is fine for small sums, but cheap checks often settle the answer first. If is odd, return False before allocating anything. If the largest number exceeds , it cannot fit on either side, so return False. Once dp[target] becomes true, stop processing further numbers. For speed, store the reachable sums as bits of one integer: bit set means sum is reachable, and adding ORs the integer with a copy of itself shifted left by bits. One shift updates every sum at once, roughly 64 sums per machine word. None of this changes the worst case, since subset sum is NP-complete, but it makes typical inputs fast.
Three optimizations: (1) bail out immediately if is odd; (2) use a 1D boolean array iterated from down to for space; (3) represent the DP as a bitset and update via bit shifts, giving an extra constant-factor speedup — word-RAM time. Early termination when becomes true saves further passes.
Can [1, 2, 3, 5] be partitioned?