Interview prep · Lesson 51 of 62 · about 60 min
DP patterns: state machines, knapsack & strings
The DP families interviewers draw from after the basics: the stock-trading state machine (cooldown, fees, k transactions), the knapsack family and why loop order decides combinations vs sequences, Target Sum as subset counting, and string DP for palindromes, distinct subsequences and interleaving.
On this page · 9 sections
Family 1: state machines#
The stock problems all describe a person who is in one of a few modes on each day: holding a share, or not. The rules (cooldown, fee, limited transactions) only change which modes exist and which moves between them are allowed. Draw the modes, then write one variable per mode:
Drawing diagram…
Each arrow becomes one term in a max. Every state is updated from yesterday's values, so assign them all at once with Go's tuple assignment. hold, sold, rest = max(hold, rest-p), hold+p, max(rest, sold) evaluates every right-hand side before assigning, so you don't need temporary copies.
Run the trace
package main
import "fmt"
func main() {
prices := []int{1, 2, 3, 0, 2}
// hold: best profit while owning a share; sold: just sold today (must rest tomorrow); rest: free to buy.
hold, sold, rest := -prices[0], 0, 0
fmt.Printf("day 0 price=%d hold=%d sold=%d rest=%d\n", prices[0], hold, sold, rest)
for i, p := range prices[1:] {
hold, sold, rest = max(hold, rest-p), hold+p, max(rest, sold)
fmt.Printf("day %d price=%d hold=%d sold=%d rest=%d\n", i+1, p, hold, sold, rest)
}
fmt.Println("best:", max(sold, rest))
}day 0 price=1 hold=-1 sold=0 rest=0
day 1 price=2 hold=-1 sold=1 rest=0
day 2 price=3 hold=-1 sold=2 rest=1
day 3 price=0 hold=1 sold=-1 rest=2
day 4 price=2 hold=1 sold=3 rest=2
best: 3Day 3 shows the cooldown working: hold becomes 1 by buying at 0 from rest (2), and not from yesterday's sold (2). The best plan is buy, sell, cooldown, buy, sell.
| Variant | States | Change |
|---|---|---|
| unlimited transactions | hold, free | free = max(free, hold + p) |
| with fee | hold, free | subtract the fee when selling |
| with cooldown | hold, sold, rest | buy only from rest |
| at most k transactions | hold[j], free[j] for j = 1..k | buying moves you from free[j−1] to hold[j] |
Family 2: knapsack, and why loop order matters#
"Choose items so their total is X" is a knapsack. Two questions decide the code:
| Can an item be reused? | Does order matter? | Loops | Example |
|---|---|---|---|
| no (0/1) | no | items outer, sums downward | Partition Equal Subset Sum, Target Sum |
| yes (unbounded) | no: combinations | items outer, sums upward | Coin Change II |
| yes | yes: sequences | sums outer, items inner | Combination Sum IV, Climbing Stairs |
Downward iteration in 0/1 knapsack keeps dp[s−n] at its value from before item n, so each item is used at most once. Upward iteration lets dp[s−n] already include n, which is exactly reuse.
The combination-vs-sequence difference is the one interviewers like to probe. Run both on the same input:
package main
import "fmt"
func main() {
coins, amount := []int{1, 2}, 3
// Coins in the outer loop: each coin is considered once, in a fixed order -> combinations.
comb := make([]int, amount+1)
comb[0] = 1
for _, c := range coins {
for a := c; a <= amount; a++ {
comb[a] += comb[a-c]
}
}
// Amount in the outer loop: any coin can be last at every step -> ordered sequences.
perm := make([]int, amount+1)
perm[0] = 1
for a := 1; a <= amount; a++ {
for _, c := range coins {
if c <= a {
perm[a] += perm[a-c]
}
}
}
fmt.Println("combinations:", comb[amount], "(1+1+1, 1+2)")
fmt.Println("sequences: ", perm[amount], "(1+1+1, 1+2, 2+1)")
}combinations: 2 (1+1+1, 1+2)
sequences: 3 (1+1+1, 1+2, 2+1)With coins in the outer loop, all the 1s are placed before any 2, so each multiset is built in exactly one order. With amounts in the outer loop, every coin can be the last one at every amount, so 1+2 and 2+1 count separately.
Target Sum is a disguised knapsack
"Put + or − before each number to reach target." Let P be the sum of the numbers with +, and N the sum of those with −. Then P − N = target and P + N = total, so P = (total + target) / 2. If that is negative or not an integer, there are 0 ways; otherwise, count subsets that sum to P. That is a 0/1 knapsack with sums iterated downward. Rewriting a problem into a known family like this is the core skill in this lesson.
Family 3: string DP#
Two shapes cover most string problems:
- One string, a range
[i, j]: palindromes.dp[i][j]describess[i..j], and you fill it from short ranges to long ones by loopingidownward andjupward. Longest palindromic subsequence: if the ends match,dp[i+1][j−1] + 2; otherwisemax(dp[i+1][j], dp[i][j−1]). - Two strings, prefixes
(i, j):dp[i][j]describesa[:i]andb[:j], as in LCS and edit distance. Distinct Subsequences counts the wayst[:j]appears ins[:i]: always skips[i], and if it matchest[j], also use it. Interleaving String asks whether the last character ofs3[:i+j]could come froms1or froms2.
For counting palindromic substrings, a table is overkill. Every palindrome has a center (a character, or the gap between two), so expand from each of the 2n − 1 centers while the ends match. That's O(n²) time with O(1) space.
The full program#
package main
import (
"fmt"
"math"
)
// State machines: one variable per state, all updated together each day.
// maxProfitUnlimited: any number of transactions.
func maxProfitUnlimited(prices []int) int {
hold, free := math.MinInt/2, 0
for _, p := range prices {
hold, free = max(hold, free-p), max(free, hold+p)
}
return free
}
// maxProfitCooldown: after selling you must wait one day.
func maxProfitCooldown(prices []int) int {
hold, sold, rest := math.MinInt/2, 0, 0
for _, p := range prices {
hold, sold, rest = max(hold, rest-p), hold+p, max(rest, sold)
}
return max(sold, rest)
}
// maxProfitFee: pay fee on every sale.
func maxProfitFee(prices []int, fee int) int {
hold, free := math.MinInt/2, 0
for _, p := range prices {
hold, free = max(hold, free-p), max(free, hold+p-fee)
}
return free
}
// maxProfitK: at most k transactions. hold[j], free[j] = states having started j buys.
func maxProfitK(k int, prices []int) int {
hold := make([]int, k+1)
free := make([]int, k+1)
for j := range hold {
hold[j] = math.MinInt / 2
}
for _, p := range prices {
for j := k; j >= 1; j-- {
free[j] = max(free[j], hold[j]+p)
hold[j] = max(hold[j], free[j-1]-p)
}
}
return free[k]
}
// Knapsack family.
// coinChange2 counts combinations (order doesn't matter): coins outer.
func coinChange2(amount int, coins []int) int {
dp := make([]int, amount+1)
dp[0] = 1
for _, c := range coins {
for a := c; a <= amount; a++ {
dp[a] += dp[a-c]
}
}
return dp[amount]
}
// combinationSum4 counts ordered sequences: amount outer.
func combinationSum4(nums []int, target int) int {
dp := make([]int, target+1)
dp[0] = 1
for a := 1; a <= target; a++ {
for _, n := range nums {
if n <= a {
dp[a] += dp[a-n]
}
}
}
return dp[target]
}
// findTargetSumWays: choose signs so the total is target. Positives P satisfy P = (sum+target)/2,
// so count subsets with that sum: 0/1 knapsack, iterating sums downward so each number is used once.
func findTargetSumWays(nums []int, target int) int {
sum := 0
for _, n := range nums {
sum += n
}
if target > sum -target > sum (sum+target)%2 != 0 {
return 0
}
want := (sum + target) / 2
dp := make([]int, want+1)
dp[0] = 1
for _, n := range nums {
for s := want; s >= n; s-- {
dp[s] += dp[s-n]
}
}
return dp[want]
}
// String DP.
// countPalindromes expands around each of the 2n-1 centers.
func countPalindromes(s string) int {
count := 0
for center := 0; center < 2*len(s)-1; center++ {
l, r := center/2, center/2+center%2
for l >= 0 r < len(s) s[l] == s[r] {
count++
l--
r++
}
}
return count
}
// longestPalSubseq: dp[i][j] = longest palindromic subsequence of s[i..j].
func longestPalSubseq(s string) int {
n := len(s)
dp := make([][]int, n)
for i := range dp {
dp[i] = make([]int, n)
}
for i := n - 1; i >= 0; i-- {
dp[i][i] = 1
for j := i + 1; j < n; j++ {
if s[i] == s[j] {
dp[i][j] = dp[i+1][j-1] + 2
} else {
dp[i][j] = max(dp[i+1][j], dp[i][j-1])
}
}
}
return dp[0][n-1]
}
// numDistinct: ways to form t as a subsequence of s. dp[j] = ways to form t[:j]; j goes down.
func numDistinct(s, t string) int {
dp := make([]int, len(t)+1)
dp[0] = 1
for i := 0; i < len(s); i++ {
for j := len(t); j >= 1; j-- {
if s[i] == t[j-1] {
dp[j] += dp[j-1]
}
}
}
return dp[len(t)]
}
// isInterleave: dp[i][j] = s1[:i] and s2[:j] can form s3[:i+j].
func isInterleave(s1, s2, s3 string) bool {
if len(s1)+len(s2) != len(s3) {
return false
}
dp := make([][]bool, len(s1)+1)
for i := range dp {
dp[i] = make([]bool, len(s2)+1)
}
dp[0][0] = true
for i := 0; i <= len(s1); i++ {
for j := 0; j <= len(s2); j++ {
if i > 0 s1[i-1] == s3[i+j-1] dp[i-1][j] {
dp[i][j] = true
}
if j > 0 s2[j-1] == s3[i+j-1] dp[i][j-1] {
dp[i][j] = true
}
}
}
return dp[len(s1)][len(s2)]
}
func main() {
fmt.Println(maxProfitUnlimited([]int{7, 1, 5, 3, 6, 4}), maxProfitCooldown([]int{1, 2, 3, 0, 2}), maxProfitFee([]int{1, 3, 2, 8, 4, 9}, 2), maxProfitK(2, []int{3, 2, 6, 5, 0, 3}))
fmt.Println(coinChange2(5, []int{1, 2, 5}), combinationSum4([]int{1, 2, 3}, 4), findTargetSumWays([]int{1, 1, 1, 1, 1}, 3))
fmt.Println(countPalindromes("aaa"), longestPalSubseq("bbbab"), numDistinct("rabbbit", "rabbit"))
fmt.Println(isInterleave("aabcc", "dbbca", "aadbbcbcac"), isInterleave("aabcc", "dbbca", "aadbbbaccc"))
}7 3 8 7
4 7 5
6 4 3
true falseComplexity#
| Problem | Time | Space |
|---|---|---|
| stocks: unlimited, fee, cooldown | O(n) | O(1) |
| stocks: k transactions | O(n · k) | O(k) |
| coin change II, combination sum IV | O(amount · coins) | O(amount) |
| target sum | O(n · total) | O(total) |
| palindromic substrings | O(n²) | O(1) |
| longest palindromic subsequence | O(n²) | O(n²), or O(n) with two rows |
| distinct subsequences | O(len(s) · len(t)) | O(len(t)) |
| interleaving string | O(len(s1) · len(s2)) | O(len(s1) · len(s2)) |
Recognizing the pattern#
- "Each day you can buy, sell, rest" or any problem with modes and rules for switching: state machine. Draw the states first.
- "Choose numbers to reach a target," "partition into two groups," "number of ways to make change": knapsack. Then ask whether items can be reused and whether order matters.
- "Palindrome" plus "subsequence" or "substring": range DP or expanding around centers.
- "Two strings" plus "subsequence," "transform" or "interleave": prefix DP over (i, j).
Summary#
- State machines: one variable per mode, all updated together from yesterday's values with tuple assignment.
- Start
holdat a very negative value, not 0. - Knapsack: sums downward for 0/1, upward for unbounded; items outer for combinations, sums outer for sequences.
- Target Sum is subset counting with P = (total + target) / 2.
- String DP is either a range
[i, j](palindromes) or two prefixes(i, j)(two strings). - Collapse 2D tables to 1D when each row needs only the previous one, choosing the loop direction carefully.
Check your understanding
Answer before scrolling back up. If you miss one, reread the matching section, then try again.
Q1.Stock with cooldown: why does
holdbuy fromrestand not fromsold?Q2.Coins {1, 2}, amount 3. Coins in the outer loop gives 2 and amount in the outer loop gives 3. Why?
Q3.0/1 knapsack in a 1D array: why iterate the sum downward?
Q4.Target Sum (assign + or − to each number to reach target) becomes which simpler problem?
Q5.Longest palindromic subsequence: when s[i] == s[j], dp[i][j] equals:
Practice in Go
Write a table test first, then the solution. On LeetCode, choose Go as the language. Open a hint only after 15 minutes stuck.
Show hint
Two states, hold and free, updated together each day.
Show hint
Three states: hold, sold today, rest. Buy only from rest.
- Coin Change II ↗medium
Show hint
Coins in the outer loop, amounts upward: unbounded combinations.
- Combination Sum IV ↗medium
Show hint
Amount in the outer loop: ordered sequences.
- Target Sum ↗medium
Show hint
Count subsets with sum (total + target) / 2, sums iterated downward.
- Palindromic Substrings ↗medium
Show hint
Expand around 2n−1 centers. O(n²) time, O(1) space.
- Interleaving String ↗medium
Show hint
dp[i][j]: the last character of s3[:i+j] came from s1 or from s2.
Show hint
dp[j] = ways to form t[:j]; for each character of s, j goes downward.
Show hint
hold[j] and free[j] for j = 1..k. If k ≥ n/2, it is the unlimited version.
Finished this lesson?
Mark it done to track your progress. It is saved in this browser only.