Go Guide

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
  1. 01Family 1: state machines
  2. 02Family 2: knapsack, and why loop order matters
  3. 03Family 3: string DP
  4. 04The full program
  5. 05Complexity
  6. 06Recognizing the pattern
  7. 07Summary
  8. 08Check your understanding
  9. 09Practice in Go

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

cooldown_trace.go
Go
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))
}
Output
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: 3

Day 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.

VariantStatesChange
unlimited transactionshold, freefree = max(free, hold + p)
with feehold, freesubtract the fee when selling
with cooldownhold, sold, restbuy only from rest
at most k transactionshold[j], free[j] for j = 1..kbuying 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?LoopsExample
no (0/1)noitems outer, sums downwardPartition Equal Subset Sum, Target Sum
yes (unbounded)no: combinationsitems outer, sums upwardCoin Change II
yesyes: sequencessums outer, items innerCombination 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:

loop_order.go
Go
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)")
}
Output
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] describes s[i..j], and you fill it from short ranges to long ones by looping i downward and j upward. Longest palindromic subsequence: if the ends match, dp[i+1][j−1] + 2; otherwise max(dp[i+1][j], dp[i][j−1]).
  • Two strings, prefixes (i, j): dp[i][j] describes a[:i] and b[:j], as in LCS and edit distance. Distinct Subsequences counts the ways t[:j] appears in s[:i]: always skip s[i], and if it matches t[j], also use it. Interleaving String asks whether the last character of s3[:i+j] could come from s1 or from s2.

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#

dp_patterns.go
Go
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"))
}
Output
7 3 8 7
4 7 5
6 4 3
true false

Complexity#

ProblemTimeSpace
stocks: unlimited, fee, cooldownO(n)O(1)
stocks: k transactionsO(n · k)O(k)
coin change II, combination sum IVO(amount · coins)O(amount)
target sumO(n · total)O(total)
palindromic substringsO(n²)O(1)
longest palindromic subsequenceO(n²)O(n²), or O(n) with two rows
distinct subsequencesO(len(s) · len(t))O(len(t))
interleaving stringO(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 hold at 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.

  1. Q1.Stock with cooldown: why does hold buy from rest and not from sold?

  2. Q2.Coins {1, 2}, amount 3. Coins in the outer loop gives 2 and amount in the outer loop gives 3. Why?

  3. Q3.0/1 knapsack in a 1D array: why iterate the sum downward?

  4. Q4.Target Sum (assign + or − to each number to reach target) becomes which simpler problem?

  5. 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.

Finished this lesson?

Mark it done to track your progress. It is saved in this browser only.