Dynamic Programming: 1D
Solve each subproblem once: define a state, write a recurrence from the last decision, then fill a table from the smallest case up.
How It Works
Dynamic programming is recursion that remembers. Plenty of recursive solutions ask the same question over and over: counting the ways up 5 stairs asks about 3 stairs twice and about 2 stairs three times. DP answers each distinct question once, stores the answer and looks it up from then on. In 1D problems, one number identifies each question: an index into the input, a step, an amount of money.
ways(5)
+---------+---------+
ways(4) ways(3)
+----+----+ +----+----+
ways(3) ways(2) ways(2) ways(1)
+---+---+
ways(2) ways(1)A DP solution needs a state, a precise description of one subproblem, and a recurrence, a rule that builds a state's answer from smaller states. Work them out in this order before writing any code:
- Define the state in one sentence.
dp[i]= the number of ways to reach stepi. If you can't say exactly whatdp[i]means, the code that fills it will be wrong in ways that are hard to spot. - Find the recurrence by asking what the last decision was. The last move onto step
iwas either 1 step (fromi - 1) or 2 steps (fromi - 2). Those cases don't overlap and they cover everything, sodp[i] = dp[i - 1] + dp[i - 2]. - Set the base cases, the smallest states, which you answer directly.
dp[0] = 1(taking no moves is the one way to stand on step 0) anddp[1] = 1. - Choose an order that has every state's inputs ready before the state itself. Here that's increasing
i. - Read off the answer. Here it's
dp[n]. In other problems it might be the best value over alldp[i].
Filling the Climbing Stairs Table
Step 1 of 7
dp
- Entry being filled
- Entries it reads
- Answer
dp[i] is the number of ways to reach step i. For n = 5 the table needs 6 entries, one for each step from 0 to 5.
All 7 steps as text
dp[i]is the number of ways to reach stepi. Forn = 5the table needs6entries, one for each step from0to5.- Set the base cases directly.
dp[0] = 1, because taking no moves is the one way to stand on step 0, anddp[1] = 1, a single 1-step move. dp[2] = dp[1] + dp[0] = 1 + 1 = 2. The last move onto step2was 1 step from step1or 2 steps from step0, and those cases never overlap.dp[3] = dp[2] + dp[1] = 2 + 1 = 3. Both inputs are already final, because the loop fills the table in increasingi.dp[4] = dp[3] + dp[2] = 3 + 2 = 5. Plain recursion would solveways(2)again insideways(3). The table just reads it.dp[5] = dp[4] + dp[3] = 5 + 3 = 8. Plain recursion would solveways(3)again insideways(4). The table just reads it.dp[5] = 8is the answer. Each entry was computed once from two lookups, so the fill is O(n). Each one read only the two entries before it, which is why two variables can replace the table.
Then decide how to compute it. Top-down (memoization) is the recursive function plus a cache, and it only visits states it actually needs. Bottom-up (tabulation) fills an array with a loop, with no recursion depth to worry about. When dp[i] only reads a fixed number of earlier entries, keep just those in variables and the space drops to O(1).
When to Reach for It
- The question asks how many ways, or for the fewest, most or cheapest, over a sequence of choices.
- Each decision depends only on what happened at the last one or two positions, like not robbing two neighboring houses.
- A brute-force recursion would ask about the same index or amount many times.
- A greedy rule looks tempting, but a small counterexample breaks it, as with coins
[1, 3, 4]and amount6.
Complexity
| Operation | Time | Space |
|---|---|---|
| Plain recursion on Climbing Stairs | O(2^n) | O(n) call stack |
| Memoized recursion | O(n) | O(n) cache and call stack |
| Bottom-up table | O(n) | O(n) |
| Rolling variables | O(n) | O(1) |
| Any DP, as a rule of thumb | states × work per state | states you keep |
The Template
Climbing Stairs, Three Ways
Count the distinct ways to climb n stairs when each move is 1 or 2 steps (LeetCode 70). The state is ways(i), the number of ways to reach step i, and the recurrence is ways(i) = ways(i - 1) + ways(i - 2) with ways(0) = ways(1) = 1. LeetCode caps n at 45, the last value whose answer fits in a 32-bit int. The file computes it three ways, from most direct to most compact:
- Memoized recursion. The recurrence written as a function, plus a
memoarray. The first call for eachidoes the work and every later call returns the stored value, so each of the n states is computed once.0marks an unsolved entry, which is safe only because every real answer is at least 1. - Bottom-up table. The same values in a
dparray, filled from step 2 upward. By the time the loop reachesi,dp[i - 1]anddp[i - 2]are already final, and there's no recursion to overflow the stack. - Rolling variables.
dp[i]only reads the two entries just before it, so the rest of the table is dead weight. Two variables,twoBackandoneBack, slide up the stairs instead.
i 0 1 2 3 4 5
dp[i] 1 1 2 3 5 8
^ ^ ^
3 + 5 = 8Time O(n)Space O(n) for memo and table, O(1) rolling
# Number of ways to climb n stairs (n >= 1) taking 1 or 2 steps at a time,
# computed three ways. All three use ways(i) = ways(i - 1) + ways(i - 2).
def climb_stairs_memo(n: int) -> int:
"""1. Memoized recursion: the recurrence as written, plus a cache so each
step is solved only once."""
memo = [0] * (n + 1) # 0 means unsolved: every real answer is at least 1.
return ways(n, memo)
def ways(i: int, memo: list[int]) -> int:
if i <= 1:
return 1 # One way to stand on step 0 (do nothing) and one to reach step 1.
if memo[i] == 0:
memo[i] = ways(i - 1, memo) + ways(i - 2, memo)
return memo[i]
def climb_stairs_table(n: int) -> int:
"""2. Bottom-up table: fill dp from the bottom step up, so both inputs to
dp[i] are always ready."""
dp = [0] * (n + 1) # dp[i] = ways to reach step i
dp[0] = 1
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2] # The last move was either one step or two.
return dp[n]
def climb_stairs_rolling(n: int) -> int:
"""3. Rolling variables: dp[i] only reads the two entries before it, so keep
just those two."""
two_back = 1 # ways(i - 2), starting at ways(0)
one_back = 1 # ways(i - 1), starting at ways(1)
for _ in range(2, n + 1):
here = one_back + two_back # ways(i)
two_back = one_back
one_back = here
return one_back
if __name__ == "__main__":
# Prints 1 1 1, 2 2 2, 3 3 3, then 1836311903 three times.
for n in [1, 2, 3, 45]:
print(climb_stairs_memo(n), climb_stairs_table(n), climb_stairs_rolling(n))// Number of ways to climb n stairs (n >= 1) taking 1 or 2 steps at a time,
// computed three ways. All three use ways(i) = ways(i - 1) + ways(i - 2).
// 1. Memoized recursion: the recurrence as written, plus a cache so each step
// is solved only once.
export function climbStairsMemo(n: number): number {
const memo = new Array<number>(n + 1).fill(0); // 0 means unsolved: every real answer is at least 1.
return ways(n, memo);
}
function ways(i: number, memo: number[]): number {
if (i <= 1) return 1; // One way to stand on step 0 (do nothing) and one to reach step 1.
if (memo[i] === 0) memo[i] = ways(i - 1, memo) + ways(i - 2, memo);
return memo[i];
}
// 2. Bottom-up table: fill dp from the bottom step up, so both inputs to
// dp[i] are always ready.
export function climbStairsTable(n: number): number {
const dp = new Array<number>(n + 1).fill(0); // dp[i] = ways to reach step i
dp[0] = 1;
dp[1] = 1;
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]; // The last move was either one step or two.
}
return dp[n];
}
// 3. Rolling variables: dp[i] only reads the two entries before it, so keep
// just those two.
export function climbStairsRolling(n: number): number {
let twoBack = 1; // ways(i - 2), starting at ways(0)
let oneBack = 1; // ways(i - 1), starting at ways(1)
for (let i = 2; i <= n; i++) {
const here = oneBack + twoBack; // ways(i)
twoBack = oneBack;
oneBack = here;
}
return oneBack;
}
// Prints 1 1 1, 2 2 2, 3 3 3, then 1836311903 three times.
for (const n of [1, 2, 3, 45]) {
console.log(climbStairsMemo(n), climbStairsTable(n), climbStairsRolling(n));
}// Number of ways to climb n stairs (n >= 1) taking 1 or 2 steps at a time,
// computed three ways. All three use ways(i) = ways(i - 1) + ways(i - 2).
package main
import "fmt"
// climbStairsMemo is memoized recursion: the recurrence as written, plus a
// cache so each step is solved only once.
func climbStairsMemo(n int) int {
memo := make([]int, n+1) // 0 means unsolved: every real answer is at least 1.
return ways(n, memo)
}
func ways(i int, memo []int) int {
if i <= 1 {
return 1 // One way to stand on step 0 (do nothing) and one to reach step 1.
}
if memo[i] == 0 {
memo[i] = ways(i-1, memo) + ways(i-2, memo)
}
return memo[i]
}
// climbStairsTable is a bottom-up table: fill dp from the bottom step up, so
// both inputs to dp[i] are always ready.
func climbStairsTable(n int) int {
dp := make([]int, n+1) // dp[i] = ways to reach step i
dp[0] = 1
dp[1] = 1
for i := 2; i <= n; i++ {
dp[i] = dp[i-1] + dp[i-2] // The last move was either one step or two.
}
return dp[n]
}
// climbStairsRolling keeps rolling variables: dp[i] only reads the two
// entries before it, so keep just those two.
func climbStairsRolling(n int) int {
twoBack := 1 // ways(i - 2), starting at ways(0)
oneBack := 1 // ways(i - 1), starting at ways(1)
for i := 2; i <= n; i++ {
here := oneBack + twoBack // ways(i)
twoBack = oneBack
oneBack = here
}
return oneBack
}
func main() {
// Prints 1 1 1, 2 2 2, 3 3 3, then 1836311903 three times.
for _, n := range []int{1, 2, 3, 45} {
fmt.Println(climbStairsMemo(n), climbStairsTable(n), climbStairsRolling(n))
}
}using System;
// Number of ways to climb n stairs (n >= 1) taking 1 or 2 steps at a time,
// computed three ways. All three use ways(i) = ways(i - 1) + ways(i - 2).
public class Solution
{
// 1. Memoized recursion: the recurrence as written, plus a cache so each
// step is solved only once.
public static int ClimbStairsMemo(int n)
{
var memo = new int[n + 1]; // 0 means unsolved: every real answer is at least 1.
return Ways(n, memo);
}
static int Ways(int i, int[] memo)
{
if (i <= 1) return 1; // One way to stand on step 0 (do nothing) and one to reach step 1.
if (memo[i] == 0) memo[i] = Ways(i - 1, memo) + Ways(i - 2, memo);
return memo[i];
}
// 2. Bottom-up table: fill dp from the bottom step up, so both inputs to
// dp[i] are always ready.
public static int ClimbStairsTable(int n)
{
var dp = new int[n + 1]; // dp[i] = ways to reach step i
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++)
{
dp[i] = dp[i - 1] + dp[i - 2]; // The last move was either one step or two.
}
return dp[n];
}
// 3. Rolling variables: dp[i] only reads the two entries before it, so keep
// just those two.
public static int ClimbStairsRolling(int n)
{
int twoBack = 1; // ways(i - 2), starting at ways(0)
int oneBack = 1; // ways(i - 1), starting at ways(1)
for (int i = 2; i <= n; i++)
{
int here = oneBack + twoBack; // ways(i)
twoBack = oneBack;
oneBack = here;
}
return oneBack;
}
public static void Main()
{
// Prints 1 1 1, 2 2 2, 3 3 3, then 1836311903 three times.
foreach (int n in new[] { 1, 2, 3, 45 })
{
Console.WriteLine($"{ClimbStairsMemo(n)} {ClimbStairsTable(n)} {ClimbStairsRolling(n)}");
}
}
}// Number of ways to climb n stairs (n >= 1) taking 1 or 2 steps at a time,
// computed three ways. All three use ways(i) = ways(i - 1) + ways(i - 2).
public class Solution {
// 1. Memoized recursion: the recurrence as written, plus a cache so each
// step is solved only once.
static int climbStairsMemo(int n) {
int[] memo = new int[n + 1]; // 0 means unsolved: every real answer is at least 1.
return ways(n, memo);
}
static int ways(int i, int[] memo) {
if (i <= 1) return 1; // One way to stand on step 0 (do nothing) and one to reach step 1.
if (memo[i] == 0) memo[i] = ways(i - 1, memo) + ways(i - 2, memo);
return memo[i];
}
// 2. Bottom-up table: fill dp from the bottom step up, so both inputs to
// dp[i] are always ready.
static int climbStairsTable(int n) {
int[] dp = new int[n + 1]; // dp[i] = ways to reach step i
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]; // The last move was either one step or two.
}
return dp[n];
}
// 3. Rolling variables: dp[i] only reads the two entries before it, so keep
// just those two.
static int climbStairsRolling(int n) {
int twoBack = 1; // ways(i - 2), starting at ways(0)
int oneBack = 1; // ways(i - 1), starting at ways(1)
for (int i = 2; i <= n; i++) {
int here = oneBack + twoBack; // ways(i)
twoBack = oneBack;
oneBack = here;
}
return oneBack;
}
public static void main(String[] args) {
// Prints 1 1 1, 2 2 2, 3 3 3, then 1836311903 three times.
for (int n : new int[] {1, 2, 3, 45}) {
System.out.println(climbStairsMemo(n) + " " + climbStairsTable(n) + " " + climbStairsRolling(n));
}
}
}Worked Problems
Each solution stays folded until you open it, so you can try the problem first. Every one is a complete program in Python, TypeScript, Go, C# and Java, run and checked against its expected output.
Problem
Each house on a street holds some amount of money, given in order as nums. You can rob any set of houses except two that sit next to each other, because that trips the alarm. Return the most money you can take.
Example: [2, 7, 9, 3, 1] returns 12, by robbing houses 0, 2 and 4 for 2 + 9 + 1.
Show SolutionHide Solution
Approach
State: best(i) = the most money you can take from the first i houses. Now ask what happened to the last of them, house i - 1. Either you skipped it, and the answer is best(i - 1), or you robbed it, which rules out house i - 2 and leaves best(i - 2) + nums[i - 1]. Take the larger of the two.
i 0 1 2 3 4 5
house 2 7 9 3 1
best(i) 0 2 7 11 11 12
best(3) = max(best(2), best(1) + 9)
= max(7, 2 + 9) = 11Each value reads only the two before it, so two variables replace the table: oneBack holds best(i - 1) and twoBack holds best(i - 2). Both start at 0, because before the first house there's nothing to take.
Time O(n)Space O(1)
def rob(nums: list[int]) -> int:
"""Most money you can take from a row of houses without robbing two neighbors."""
# best(i) = most money from the first i houses
# = max(best(i - 1), best(i - 2) + nums[i - 1])
# Each value reads only the two before it, so keep just those two.
two_back = 0 # best(i - 2)
one_back = 0 # best(i - 1)
for money in nums:
# Skip this house and keep one_back, or rob it on top of two_back.
here = max(one_back, two_back + money)
two_back = one_back
one_back = here
return one_back
if __name__ == "__main__":
print(rob([1, 2, 3, 1])) # 4
print(rob([2, 7, 9, 3, 1])) # 12
print(rob([2, 1, 1, 2])) # 4// Most money you can take from a row of houses without robbing two neighbors.
export function rob(nums: number[]): number {
// best(i) = most money from the first i houses
// = max(best(i - 1), best(i - 2) + nums[i - 1])
// Each value reads only the two before it, so keep just those two.
let twoBack = 0; // best(i - 2)
let oneBack = 0; // best(i - 1)
for (const money of nums) {
// Skip this house and keep oneBack, or rob it on top of twoBack.
const here = Math.max(oneBack, twoBack + money);
twoBack = oneBack;
oneBack = here;
}
return oneBack;
}
console.log(rob([1, 2, 3, 1])); // 4
console.log(rob([2, 7, 9, 3, 1])); // 12
console.log(rob([2, 1, 1, 2])); // 4package main
import "fmt"
// rob returns the most money you can take from a row of houses without
// robbing two neighbors.
func rob(nums []int) int {
// best(i) = most money from the first i houses
// = max(best(i - 1), best(i - 2) + nums[i - 1])
// Each value reads only the two before it, so keep just those two.
twoBack := 0 // best(i - 2)
oneBack := 0 // best(i - 1)
for _, money := range nums {
// Skip this house and keep oneBack, or rob it on top of twoBack.
here := max(oneBack, twoBack+money)
twoBack = oneBack
oneBack = here
}
return oneBack
}
func main() {
fmt.Println(rob([]int{1, 2, 3, 1})) // 4
fmt.Println(rob([]int{2, 7, 9, 3, 1})) // 12
fmt.Println(rob([]int{2, 1, 1, 2})) // 4
}using System;
public class Solution
{
// Most money you can take from a row of houses without robbing two neighbors.
public static int Rob(int[] nums)
{
// best(i) = most money from the first i houses
// = max(best(i - 1), best(i - 2) + nums[i - 1])
// Each value reads only the two before it, so keep just those two.
int twoBack = 0; // best(i - 2)
int oneBack = 0; // best(i - 1)
foreach (int money in nums)
{
// Skip this house and keep oneBack, or rob it on top of twoBack.
int here = Math.Max(oneBack, twoBack + money);
twoBack = oneBack;
oneBack = here;
}
return oneBack;
}
public static void Main()
{
Console.WriteLine(Rob(new[] { 1, 2, 3, 1 })); // 4
Console.WriteLine(Rob(new[] { 2, 7, 9, 3, 1 })); // 12
Console.WriteLine(Rob(new[] { 2, 1, 1, 2 })); // 4
}
}public class Solution {
// Most money you can take from a row of houses without robbing two neighbors.
static int rob(int[] nums) {
// best(i) = most money from the first i houses
// = max(best(i - 1), best(i - 2) + nums[i - 1])
// Each value reads only the two before it, so keep just those two.
int twoBack = 0; // best(i - 2)
int oneBack = 0; // best(i - 1)
for (int money : nums) {
// Skip this house and keep oneBack, or rob it on top of twoBack.
int here = Math.max(oneBack, twoBack + money);
twoBack = oneBack;
oneBack = here;
}
return oneBack;
}
public static void main(String[] args) {
System.out.println(rob(new int[] {1, 2, 3, 1})); // 4
System.out.println(rob(new int[] {2, 7, 9, 3, 1})); // 12
System.out.println(rob(new int[] {2, 1, 1, 2})); // 4
}
}Problem
You have coins in the denominations listed in coins, with an unlimited supply of each. Return the fewest coins that add up to exactly amount, or -1 if no combination of the coins can make it.
Example: coins = [1, 2, 5] and amount = 11 returns 3, from 5 + 5 + 1.
Show SolutionHide Solution
Approach
Taking the biggest coin that fits works for coin systems like US cents, but not in general. With coins = [1, 3, 4] and amount = 6, greedy takes 4 + 1 + 1 for 3 coins, while 3 + 3 needs only 2. You have to consider every coin as the possible last one.
State: dp[a] = the fewest coins that make amount a. If the last coin is c, the rest must be the best way to make a - c, so dp[a] = 1 + min(dp[a - c]) over every coin c <= a. The base case is dp[0] = 0, and filling a from 1 upward means every dp[a - c] is ready when it's read.
coins = [1, 3, 4]
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) = 2Amounts that no coins can reach keep the placeholder amount + 1. It works as infinity because a real answer never needs more than amount coins (all 1s), and unlike a language's maximum integer, adding 1 to it can't overflow. If dp[amount] still holds it at the end, return -1. Each of the amount states tries all k coins, so the time is O(amount · k).
Time O(amount · k)Space O(amount)
def coin_change(coins: list[int], amount: int) -> int:
"""Fewest coins that add up to exactly `amount`, with unlimited coins of each
denomination, or -1 if no combination of coins can make it."""
# Stands in for infinity. A real answer never needs more than `amount`
# coins (all 1s), and unlike a max integer, adding 1 can't overflow.
unreachable = amount + 1
dp = [unreachable] * (amount + 1) # dp[a] = fewest coins that make a
dp[0] = 0 # Zero coins make zero.
for a in range(1, amount + 1):
for coin in coins:
# If the last coin is `coin`, the rest is the best way to make a - coin.
if coin <= a:
dp[a] = min(dp[a], dp[a - coin] + 1)
return -1 if dp[amount] == unreachable else dp[amount]
if __name__ == "__main__":
print(coin_change([1, 2, 5], 11)) # 3
print(coin_change([2], 3)) # -1
print(coin_change([1], 0)) # 0
print(coin_change([1, 3, 4], 6)) # 2 (3 + 3, where greedy picks 4 + 1 + 1)// Fewest coins that add up to exactly `amount`, with unlimited coins of each
// denomination, or -1 if no combination of coins can make it.
export function coinChange(coins: number[], amount: number): number {
// Stands in for infinity. A real answer never needs more than `amount`
// coins (all 1s), and unlike a max integer, adding 1 can't overflow.
const unreachable = amount + 1;
const dp = new Array<number>(amount + 1).fill(unreachable); // dp[a] = fewest coins that make a
dp[0] = 0; // Zero coins make zero.
for (let a = 1; a <= amount; a++) {
for (const coin of coins) {
// If the last coin is `coin`, the rest is the best way to make a - coin.
if (coin <= a) dp[a] = Math.min(dp[a], dp[a - coin] + 1);
}
}
return dp[amount] === unreachable ? -1 : dp[amount];
}
console.log(coinChange([1, 2, 5], 11)); // 3
console.log(coinChange([2], 3)); // -1
console.log(coinChange([1], 0)); // 0
console.log(coinChange([1, 3, 4], 6)); // 2 (3 + 3, where greedy picks 4 + 1 + 1)package main
import "fmt"
// coinChange returns the fewest coins that add up to exactly amount, with
// unlimited coins of each denomination, or -1 if no combination can make it.
func coinChange(coins []int, amount int) int {
// Stands in for infinity. A real answer never needs more than amount
// coins (all 1s), and unlike a max integer, adding 1 can't overflow.
unreachable := amount + 1
dp := make([]int, amount+1) // dp[a] = fewest coins that make a
for a := range dp {
dp[a] = unreachable
}
dp[0] = 0 // Zero coins make zero.
for a := 1; a <= amount; a++ {
for _, coin := range coins {
// If the last coin is coin, the rest is the best way to make a - coin.
if coin <= a {
dp[a] = min(dp[a], dp[a-coin]+1)
}
}
}
if dp[amount] == unreachable {
return -1
}
return dp[amount]
}
func main() {
fmt.Println(coinChange([]int{1, 2, 5}, 11)) // 3
fmt.Println(coinChange([]int{2}, 3)) // -1
fmt.Println(coinChange([]int{1}, 0)) // 0
fmt.Println(coinChange([]int{1, 3, 4}, 6)) // 2 (3 + 3, where greedy picks 4 + 1 + 1)
}using System;
public class Solution
{
// Fewest coins that add up to exactly amount, with unlimited coins of each
// denomination, or -1 if no combination of coins can make it.
public static int CoinChange(int[] coins, int amount)
{
// Stands in for infinity. A real answer never needs more than amount
// coins (all 1s), and unlike a max integer, adding 1 can't overflow.
int unreachable = amount + 1;
var dp = new int[amount + 1]; // dp[a] = fewest coins that make a
Array.Fill(dp, unreachable);
dp[0] = 0; // Zero coins make zero.
for (int a = 1; a <= amount; a++)
{
foreach (int coin in coins)
{
// If the last coin is coin, the rest is the best way to make a - coin.
if (coin <= a) dp[a] = Math.Min(dp[a], dp[a - coin] + 1);
}
}
return dp[amount] == unreachable ? -1 : dp[amount];
}
public static void Main()
{
Console.WriteLine(CoinChange(new[] { 1, 2, 5 }, 11)); // 3
Console.WriteLine(CoinChange(new[] { 2 }, 3)); // -1
Console.WriteLine(CoinChange(new[] { 1 }, 0)); // 0
Console.WriteLine(CoinChange(new[] { 1, 3, 4 }, 6)); // 2 (3 + 3, where greedy picks 4 + 1 + 1)
}
}import java.util.Arrays;
public class Solution {
// Fewest coins that add up to exactly amount, with unlimited coins of each
// denomination, or -1 if no combination of coins can make it.
static int coinChange(int[] coins, int amount) {
// Stands in for infinity. A real answer never needs more than amount
// coins (all 1s), and unlike a max integer, adding 1 can't overflow.
int unreachable = amount + 1;
int[] dp = new int[amount + 1]; // dp[a] = fewest coins that make a
Arrays.fill(dp, unreachable);
dp[0] = 0; // Zero coins make zero.
for (int a = 1; a <= amount; a++) {
for (int coin : coins) {
// If the last coin is coin, the rest is the best way to make a - coin.
if (coin <= a) dp[a] = Math.min(dp[a], dp[a - coin] + 1);
}
}
return dp[amount] == unreachable ? -1 : dp[amount];
}
public static void main(String[] args) {
System.out.println(coinChange(new int[] {1, 2, 5}, 11)); // 3
System.out.println(coinChange(new int[] {2}, 3)); // -1
System.out.println(coinChange(new int[] {1}, 0)); // 0
System.out.println(coinChange(new int[] {1, 3, 4}, 6)); // 2 (3 + 3, where greedy picks 4 + 1 + 1)
}
}Common Pitfalls
- Writing code before you can say in one sentence what
dp[i]means. Most off-by-one bugs in DP come from a fuzzy state. - Marking unsolved memo entries with a value that can also be a real answer, such as
0when an answer can be 0. Those entries look unsolved forever, so they're recomputed on every call and the run can go exponential again. Use-1,nullor a map. - Using the maximum integer as infinity in a min-DP and then adding 1 to it. In Java, C# and Go it wraps around to a huge negative number that wins every
min. - Filling the table in an order where
dp[i]reads an entry that hasn't been computed yet. - Memoized recursion on long inputs in Python. The default recursion limit is about 1000, so a chain of 10,000 states crashes. Write it bottom-up instead.
Practice
More problems to try on your own.