Dynamic Programming: Knapsack & Subsequences
Choose items under a budget with a table indexed by capacity, and find the longest increasing subsequence from the best answer ending at each index.
How It Works
In a knapsack problem you have items, each with a weight and usually a value, and a capacity. You pick a subset of the items to get the most value without going over capacity. Greedy fails here. With capacity 5 and items (weight 1, value 6), (2, 10) and (3, 12), taking the best value per unit of weight picks the first two for 16 and leaves 2 units the last item can't use. The best answer is the last two, for 22.
DP works because the capacity is a small integer you can index a table by. State: dp[i][c] = the best value using only the first i items with capacity c. The recurrence asks about the last item: skip it and keep dp[i - 1][c], or take it, if it fits, for dp[i - 1][c - w] + v. Taking reads row i - 1, the best without this item, which is what limits each item to one use.
Filling the 0/1 Knapsack Table
Step 1 of 14
dp
- Cell being filled
- Skip and take inputs
- Ruled out
- Answer
dp[i][c] is the best value from the first i items with capacity c. Each row adds one item, written as (weight, value), and the capacity is 5.
All 14 steps as text
dp[i][c]is the best value from the firstiitems with capacityc. Each row adds one item, written as (weight, value), and the capacity is5.- With no items there's nothing to carry, so row
0is all0. Every later row is built from the row above it. - Item
(1, 6)weighs1, so it doesn't fit at capacity0. That cell can only skip it and copy the best value from the row above. dp[1][1]: skip keeps0from above. Take adds6todp[0][0], the best that fits in the0units left. Taking wins:0 + 6 = 6beats0. Take reads row0, from before this item, so it can't be used twice.- Capacities
2to5face the same choice as1: skip0or take6, so each is6. Every item so far already fits, so the extra room has nothing to add. - Item
(2, 10)weighs2, so it doesn't fit at capacity0to1. Those cells can only skip it and copy the best value from the row above. dp[2][2]: skip keeps6from above. Take adds10todp[1][0], the best that fits in the0units left. Taking wins:0 + 10 = 10beats6.dp[2][3]: skip keeps6from above. Take adds10todp[1][1], the best that fits in the1unit left. Taking wins:6 + 10 = 16beats6. That packs(2, 10)on top of earlier items worth6.- Capacities
4to5face the same choice as3: skip6or take16, so each is16. Every item so far already fits, so the extra room has nothing to add. - Item
(3, 12)weighs3, so it doesn't fit at capacity0to2. Those cells can only skip it and copy the best value from the row above. dp[3][3]: skip keeps16from above. Take adds12todp[2][0], the best that fits in the0units left. Skipping wins:16beats0 + 12 = 12.dp[3][4]: skip keeps16from above. Take adds12todp[2][1], the best that fits in the1unit left. Taking wins:6 + 12 = 18beats16. That packs(3, 12)on top of earlier items worth6.dp[3][5]: skip keeps16from above. Take adds12todp[2][2], the best that fits in the2units left. Taking wins:10 + 12 = 22beats16. That packs(3, 12)on top of earlier items worth10.dp[3][5] = 22is the answer: the best value from all3items within capacity5. Each of the24cells took O(1), so the fill is O(n · C).
- 0/1 knapsack. Each item at most once. The template below.
- Unbounded knapsack. Each item any number of times, so taking reads the current row:
dp[i][c - w] + v. Coin Change is one. - Subset sum. No values, just whether a total is reachable, so
maxbecomes OR. Partition Equal Subset Sum, below. - Counting. How many ways reach a total, so
maxbecomes+.
The other pattern here is the subsequence DP: pick elements in their original order, skipping some, so that the picked ones follow a rule such as strictly increasing. The state is dp[i] = the best answer that ends exactly at index i, and the recurrence looks back at every earlier j that can come right before it. Longest Increasing Subsequence is the model problem.
When to Reach for It
- You choose a subset of items under a budget (weight, cost, a target sum) to maximize value or to hit the target exactly.
- The question asks whether an array splits into two groups with equal sums, or how to put
+and-signs in front of numbers to reach a target. - The budget is a smallish integer, up to a few thousand, so an array indexed by it fits in memory.
- You pick elements in their original order, skipping some, to build the longest increasing or otherwise chained sequence.
- The statement says whether each item can be used once or many times. That one detail decides which way the inner loop runs.
Complexity
| Operation | Time | Space |
|---|---|---|
| 0/1 knapsack, 2D table (n items, capacity C) | O(n · C) | O(n · C) |
| 0/1 knapsack, one row | O(n · C) | O(C) |
| Subset sum to S | O(n · S) | O(S) |
| Longest Increasing Subsequence, quadratic DP | O(n²) | O(n) |
| Longest Increasing Subsequence, patience | O(n log n) | O(n) |
The Template
0/1 Knapsack
Given item weights and values and a capacity, return the most value you can carry, using each item at most once. The file solves it twice, first with the full table and then with a single row, and prints both answers.
2D table. dp[i][c] = the best value from the first i items with capacity c. Row 0 (no items) is all 0. Every other cell either skips item i - 1, copying the cell above, or takes it, adding its value to the row above at c - w.
items as (weight, value): (1, 6), (2, 10), (3, 12) capacity 0 1 2 3 4 5 no items 0 0 0 0 0 0 + (1, 6) 0 6 6 6 6 6 + (2, 10) 0 6 [10] 16 16 [16] + (3, 12) 0 6 10 16 18 (22) (22) = max(skip [16], take [10] + 12)
1D array. Row i only reads row i - 1, so one array can stand in for both: until you overwrite dp[c], it still holds the previous row's value. The catch is dp[c - w], which sits to the left. Run capacity backwards, from high to low, and the cells to the left haven't been touched for this item yet, so they still mean "best without this item".
one item: weight 2, value 10 c 0 1 2 3 4 before the item 0 0 0 0 0 forwards, c = 2..4 0 0 10 10 20 backwards, c = 4..2 0 0 10 10 10
Time O(n · C)Space O(n · C) for the table, O(C) for one row
# Most total value from items with the given weights and values, using each
# item at most once and keeping the total weight within `capacity`.
def knapsack_2d(weights: list[int], values: list[int], capacity: int) -> int:
"""2D table: dp[i][c] = best value from the first i items with capacity c."""
n = len(weights)
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
w, v = weights[i - 1], values[i - 1]
for c in range(capacity + 1):
dp[i][c] = dp[i - 1][c] # Skip this item.
if w <= c:
dp[i][c] = max(dp[i][c], dp[i - 1][c - w] + v) # Or take it, once.
return dp[n][capacity]
def knapsack_1d(weights: list[int], values: list[int], capacity: int) -> int:
"""1D array: row i only reads row i - 1, so keep a single row and update it in place."""
dp = [0] * (capacity + 1) # dp[c] = best value with capacity c
for w, v in zip(weights, values):
# Backwards, so dp[c - w] still holds the value from before this item.
# Forwards, it might already include this item: that's the unbounded knapsack.
for c in range(capacity, w - 1, -1):
dp[c] = max(dp[c], dp[c - w] + v)
return dp[capacity]
def print_both(weights: list[int], values: list[int], capacity: int) -> None:
print(knapsack_2d(weights, values, capacity), knapsack_1d(weights, values, capacity))
if __name__ == "__main__":
print_both([1, 2, 3], [6, 10, 12], 5) # 22 22
print_both([1, 3, 4, 5], [1, 4, 5, 7], 7) # 9 9
print_both([5], [10], 4) # 0 0 (the only item doesn't fit)// Most total value from items with the given weights and values, using each
// item at most once and keeping the total weight within `capacity`.
// 2D table: dp[i][c] = best value from the first i items with capacity c.
export function knapsack2D(weights: number[], values: number[], capacity: number): number {
const n = weights.length;
const dp: number[][] = Array.from({ length: n + 1 }, () => new Array<number>(capacity + 1).fill(0));
for (let i = 1; i <= n; i++) {
const w = weights[i - 1];
const v = values[i - 1];
for (let c = 0; c <= capacity; c++) {
dp[i][c] = dp[i - 1][c]; // Skip this item.
if (w <= c) dp[i][c] = Math.max(dp[i][c], dp[i - 1][c - w] + v); // Or take it, once.
}
}
return dp[n][capacity];
}
// 1D array: row i only reads row i - 1, so keep a single row and update it in place.
export function knapsack1D(weights: number[], values: number[], capacity: number): number {
const dp = new Array<number>(capacity + 1).fill(0); // dp[c] = best value with capacity c
for (let i = 0; i < weights.length; i++) {
const w = weights[i];
const v = values[i];
// Backwards, so dp[c - w] still holds the value from before this item.
// Forwards, it might already include this item: that's the unbounded knapsack.
for (let c = capacity; c >= w; c--) {
dp[c] = Math.max(dp[c], dp[c - w] + v);
}
}
return dp[capacity];
}
function printBoth(weights: number[], values: number[], capacity: number): void {
console.log(knapsack2D(weights, values, capacity), knapsack1D(weights, values, capacity));
}
printBoth([1, 2, 3], [6, 10, 12], 5); // 22 22
printBoth([1, 3, 4, 5], [1, 4, 5, 7], 7); // 9 9
printBoth([5], [10], 4); // 0 0 (the only item doesn't fit)// Most total value from items with the given weights and values, using each
// item at most once and keeping the total weight within capacity.
package main
import "fmt"
// knapsack2D fills a 2D table: dp[i][c] = best value from the first i items
// with capacity c.
func knapsack2D(weights, values []int, capacity int) int {
n := len(weights)
dp := make([][]int, n+1)
for i := range dp {
dp[i] = make([]int, capacity+1)
}
for i := 1; i <= n; i++ {
w, v := weights[i-1], values[i-1]
for c := 0; c <= capacity; c++ {
dp[i][c] = dp[i-1][c] // Skip this item.
if w <= c {
dp[i][c] = max(dp[i][c], dp[i-1][c-w]+v) // Or take it, once.
}
}
}
return dp[n][capacity]
}
// knapsack1D keeps a 1D array: row i only reads row i - 1, so keep a single
// row and update it in place.
func knapsack1D(weights, values []int, capacity int) int {
dp := make([]int, capacity+1) // dp[c] = best value with capacity c
for i := range weights {
w, v := weights[i], values[i]
// Backwards, so dp[c - w] still holds the value from before this item.
// Forwards, it might already include this item: that's the unbounded knapsack.
for c := capacity; c >= w; c-- {
dp[c] = max(dp[c], dp[c-w]+v)
}
}
return dp[capacity]
}
func printBoth(weights, values []int, capacity int) {
fmt.Println(knapsack2D(weights, values, capacity), knapsack1D(weights, values, capacity))
}
func main() {
printBoth([]int{1, 2, 3}, []int{6, 10, 12}, 5) // 22 22
printBoth([]int{1, 3, 4, 5}, []int{1, 4, 5, 7}, 7) // 9 9
printBoth([]int{5}, []int{10}, 4) // 0 0 (the only item doesn't fit)
}using System;
// Most total value from items with the given weights and values, using each
// item at most once and keeping the total weight within capacity.
public class Solution
{
// 2D table: dp[i, c] = best value from the first i items with capacity c.
public static int Knapsack2D(int[] weights, int[] values, int capacity)
{
int n = weights.Length;
var dp = new int[n + 1, capacity + 1];
for (int i = 1; i <= n; i++)
{
int w = weights[i - 1], v = values[i - 1];
for (int c = 0; c <= capacity; c++)
{
dp[i, c] = dp[i - 1, c]; // Skip this item.
if (w <= c) dp[i, c] = Math.Max(dp[i, c], dp[i - 1, c - w] + v); // Or take it, once.
}
}
return dp[n, capacity];
}
// 1D array: row i only reads row i - 1, so keep a single row and update it in place.
public static int Knapsack1D(int[] weights, int[] values, int capacity)
{
var dp = new int[capacity + 1]; // dp[c] = best value with capacity c
for (int i = 0; i < weights.Length; i++)
{
int w = weights[i], v = values[i];
// Backwards, so dp[c - w] still holds the value from before this item.
// Forwards, it might already include this item: that's the unbounded knapsack.
for (int c = capacity; c >= w; c--)
{
dp[c] = Math.Max(dp[c], dp[c - w] + v);
}
}
return dp[capacity];
}
static void PrintBoth(int[] weights, int[] values, int capacity)
{
Console.WriteLine($"{Knapsack2D(weights, values, capacity)} {Knapsack1D(weights, values, capacity)}");
}
public static void Main()
{
PrintBoth(new[] { 1, 2, 3 }, new[] { 6, 10, 12 }, 5); // 22 22
PrintBoth(new[] { 1, 3, 4, 5 }, new[] { 1, 4, 5, 7 }, 7); // 9 9
PrintBoth(new[] { 5 }, new[] { 10 }, 4); // 0 0 (the only item doesn't fit)
}
}// Most total value from items with the given weights and values, using each
// item at most once and keeping the total weight within capacity.
public class Solution {
// 2D table: dp[i][c] = best value from the first i items with capacity c.
static int knapsack2D(int[] weights, int[] values, int capacity) {
int n = weights.length;
int[][] dp = new int[n + 1][capacity + 1];
for (int i = 1; i <= n; i++) {
int w = weights[i - 1], v = values[i - 1];
for (int c = 0; c <= capacity; c++) {
dp[i][c] = dp[i - 1][c]; // Skip this item.
if (w <= c) dp[i][c] = Math.max(dp[i][c], dp[i - 1][c - w] + v); // Or take it, once.
}
}
return dp[n][capacity];
}
// 1D array: row i only reads row i - 1, so keep a single row and update it in place.
static int knapsack1D(int[] weights, int[] values, int capacity) {
int[] dp = new int[capacity + 1]; // dp[c] = best value with capacity c
for (int i = 0; i < weights.length; i++) {
int w = weights[i], v = values[i];
// Backwards, so dp[c - w] still holds the value from before this item.
// Forwards, it might already include this item: that's the unbounded knapsack.
for (int c = capacity; c >= w; c--) {
dp[c] = Math.max(dp[c], dp[c - w] + v);
}
}
return dp[capacity];
}
static void printBoth(int[] weights, int[] values, int capacity) {
System.out.println(knapsack2D(weights, values, capacity) + " " + knapsack1D(weights, values, capacity));
}
public static void main(String[] args) {
printBoth(new int[] {1, 2, 3}, new int[] {6, 10, 12}, 5); // 22 22
printBoth(new int[] {1, 3, 4, 5}, new int[] {1, 4, 5, 7}, 7); // 9 9
printBoth(new int[] {5}, new int[] {10}, 4); // 0 0 (the only item doesn't fit)
}
}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
Given an array of positive integers, decide whether it can be split into two groups with equal sums. Every number goes into exactly one of the groups.
Example: [1, 5, 11, 5] returns true, because [1, 5, 5] and [11] both sum to 11. [1, 2, 3, 5] returns false.
Show SolutionHide Solution
Approach
If the two groups are equal, each one sums to exactly half the total. So the real question is whether some subset sums to total / 2: the numbers left over automatically form the other half. An odd total can't be halved, so return false right away.
That's a 0/1 knapsack where only reachability matters. State: dp[s] = whether some subset of the numbers seen so far sums to s. Start with dp[0] = true for the empty subset. Each number num makes s reachable wherever s - num already was. Run s backwards for the same reason as in the template: each number can be used once.
nums = [1, 5, 11, 5], target = 11 s 0 1 2 3 4 5 6 7 8 9 10 11 start T . . . . . . . . . . . + 1 T T . . . . . . . . . . + 5 T T . . . T T . . . . . + 11 T T . . . T T . . . . T + 5 T T . . . T T . . . T T
With S = total / 2, the table has S + 1 entries and each of the n numbers sweeps it once, so the time is O(n · S) and the space O(S).
Time O(n · S)Space O(S)
def can_partition(nums: list[int]) -> bool:
"""Whether `nums` (positive integers) can be split into two groups with equal sums."""
total = sum(nums)
if total % 2 != 0:
return False # An odd total can't split into two equal halves.
target = total // 2 # Each group must sum to exactly half.
# dp[s] = whether some subset of the numbers seen so far sums to s.
dp = [False] * (target + 1)
dp[0] = True # The empty subset.
for num in nums:
# Backwards, as in the 0/1 knapsack, so each number is used at most once.
for s in range(target, num - 1, -1):
dp[s] = dp[s] or dp[s - num]
return dp[target]
if __name__ == "__main__":
print(can_partition([1, 5, 11, 5])) # True
print(can_partition([1, 2, 3, 5])) # False (odd total)
print(can_partition([2, 2, 3, 5])) # False (even total, but no subset makes 6)// Whether `nums` (positive integers) can be split into two groups with equal sums.
export function canPartition(nums: number[]): boolean {
const total = nums.reduce((sum, num) => sum + num, 0);
if (total % 2 !== 0) return false; // An odd total can't split into two equal halves.
const target = total / 2; // Each group must sum to exactly half.
// dp[s] = whether some subset of the numbers seen so far sums to s.
const dp = new Array<boolean>(target + 1).fill(false);
dp[0] = true; // The empty subset.
for (const num of nums) {
// Backwards, as in the 0/1 knapsack, so each number is used at most once.
for (let s = target; s >= num; s--) {
dp[s] = dp[s] || dp[s - num];
}
}
return dp[target];
}
console.log(canPartition([1, 5, 11, 5])); // true
console.log(canPartition([1, 2, 3, 5])); // false (odd total)
console.log(canPartition([2, 2, 3, 5])); // false (even total, but no subset makes 6)package main
import "fmt"
// canPartition reports whether nums (positive integers) can be split into two
// groups with equal sums.
func canPartition(nums []int) bool {
total := 0
for _, num := range nums {
total += num
}
if total%2 != 0 {
return false // An odd total can't split into two equal halves.
}
target := total / 2 // Each group must sum to exactly half.
// dp[s] = whether some subset of the numbers seen so far sums to s.
dp := make([]bool, target+1)
dp[0] = true // The empty subset.
for _, num := range nums {
// Backwards, as in the 0/1 knapsack, so each number is used at most once.
for s := target; s >= num; s-- {
dp[s] = dp[s] || dp[s-num]
}
}
return dp[target]
}
func main() {
fmt.Println(canPartition([]int{1, 5, 11, 5})) // true
fmt.Println(canPartition([]int{1, 2, 3, 5})) // false (odd total)
fmt.Println(canPartition([]int{2, 2, 3, 5})) // false (even total, but no subset makes 6)
}using System;
using System.Linq;
public class Solution
{
// Whether nums (positive integers) can be split into two groups with equal sums.
public static bool CanPartition(int[] nums)
{
int total = nums.Sum();
if (total % 2 != 0) return false; // An odd total can't split into two equal halves.
int target = total / 2; // Each group must sum to exactly half.
// dp[s] = whether some subset of the numbers seen so far sums to s.
var dp = new bool[target + 1];
dp[0] = true; // The empty subset.
foreach (int num in nums)
{
// Backwards, as in the 0/1 knapsack, so each number is used at most once.
for (int s = target; s >= num; s--)
{
dp[s] = dp[s] || dp[s - num];
}
}
return dp[target];
}
public static void Main()
{
Console.WriteLine(CanPartition(new[] { 1, 5, 11, 5 })); // True
Console.WriteLine(CanPartition(new[] { 1, 2, 3, 5 })); // False (odd total)
Console.WriteLine(CanPartition(new[] { 2, 2, 3, 5 })); // False (even total, but no subset makes 6)
}
}import java.util.Arrays;
public class Solution {
// Whether nums (positive integers) can be split into two groups with equal sums.
static boolean canPartition(int[] nums) {
int total = Arrays.stream(nums).sum();
if (total % 2 != 0) return false; // An odd total can't split into two equal halves.
int target = total / 2; // Each group must sum to exactly half.
// dp[s] = whether some subset of the numbers seen so far sums to s.
boolean[] dp = new boolean[target + 1];
dp[0] = true; // The empty subset.
for (int num : nums) {
// Backwards, as in the 0/1 knapsack, so each number is used at most once.
for (int s = target; s >= num; s--) {
dp[s] = dp[s] || dp[s - num];
}
}
return dp[target];
}
public static void main(String[] args) {
System.out.println(canPartition(new int[] {1, 5, 11, 5})); // true
System.out.println(canPartition(new int[] {1, 2, 3, 5})); // false (odd total)
System.out.println(canPartition(new int[] {2, 2, 3, 5})); // false (even total, but no subset makes 6)
}
}Problem
Return the length of the longest strictly increasing subsequence of nums. A subsequence keeps the original order but may skip elements.
Example: [10, 9, 2, 5, 3, 7, 101, 18] returns 4. One such subsequence is [2, 3, 7, 101].
Show SolutionHide Solution
Approach
O(n²) DP. State: dp[i] = the length of the longest increasing subsequence that ends at nums[i]. Fixing the end makes the recurrence easy: nums[i] can extend any subsequence that ends on a smaller value earlier in the array, so dp[i] = 1 + max(dp[j]) over every j < i with nums[j] < nums[i], or 1 if there's no such j. The answer is the largest dp[i], because the longest subsequence can end anywhere.
i 0 1 2 3 4 5 6 7 nums 10 9 2 5 3 7 101 18 dp 1 1 1 2 2 3 4 4
O(n log n) patience version. Keep tails, where tails[k] is the smallest value that ends any increasing subsequence of length k + 1 found so far. tails is always sorted, so for each x you can binary search for the first tail that's >= x. If there isn't one, x extends the longest subsequence and goes on the end. Otherwise it replaces that tail: the same length with a lower ending, which leaves more room for the numbers still to come.
x tails after x 10 [10] 9 [9] 9 replaces 10 2 [2] 2 replaces 9 5 [2, 5] bigger than every tail: append 3 [2, 3] 3 replaces 5 7 [2, 3, 7] 101 [2, 3, 7, 101] 18 [2, 3, 7, 18] 18 replaces 101
Time O(n²) for the DP, O(n log n) for patienceSpace O(n)
# Length of the longest strictly increasing subsequence of `nums`, two ways.
def lis_dp(nums: list[int]) -> int:
"""O(n^2) DP: dp[i] = length of the longest increasing subsequence ending at nums[i]."""
dp = [1] * len(nums) # Every element alone has length 1.
best = 0
for i in range(len(nums)):
for j in range(i):
# nums[i] can extend any increasing subsequence that ends on a smaller value.
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
best = max(best, dp[i]) # The longest can end anywhere, not just at the last index.
return best
def lis_tails(nums: list[int]) -> int:
"""O(n log n) patience version: tails[k] = smallest value that ends an increasing
subsequence of length k + 1. tails stays sorted, so binary search works on it."""
tails: list[int] = []
for x in nums:
# Lower bound: the first tail >= x.
lo, hi = 0, len(tails)
while lo < hi:
mid = (lo + hi) // 2
if tails[mid] < x:
lo = mid + 1
else:
hi = mid
if lo == len(tails):
tails.append(x) # x beats every tail: the longest grows by one.
else:
tails[lo] = x # Same length, lower ending: more room for later numbers.
return len(tails) # The right length, though tails itself may not be a subsequence.
if __name__ == "__main__":
# Prints 4 4, then 4 4, then 1 1.
for nums in [[10, 9, 2, 5, 3, 7, 101, 18], [0, 1, 0, 3, 2, 3], [7, 7, 7, 7, 7, 7, 7]]:
print(lis_dp(nums), lis_tails(nums))// Length of the longest strictly increasing subsequence of `nums`, two ways.
// O(n^2) DP: dp[i] = length of the longest increasing subsequence ending at nums[i].
export function lisDp(nums: number[]): number {
const dp = new Array<number>(nums.length).fill(1); // Every element alone has length 1.
let best = 0;
for (let i = 0; i < nums.length; i++) {
for (let j = 0; j < i; j++) {
// nums[i] can extend any increasing subsequence that ends on a smaller value.
if (nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
}
best = Math.max(best, dp[i]); // The longest can end anywhere, not just at the last index.
}
return best;
}
// O(n log n) patience version: tails[k] = smallest value that ends an increasing
// subsequence of length k + 1. tails stays sorted, so binary search works on it.
export function lisTails(nums: number[]): number {
const tails: number[] = [];
for (const x of nums) {
// Lower bound: the first tail >= x.
let lo = 0;
let hi = tails.length;
while (lo < hi) {
const mid = Math.floor((lo + hi) / 2);
if (tails[mid] < x) lo = mid + 1;
else hi = mid;
}
if (lo === tails.length) tails.push(x); // x beats every tail: the longest grows by one.
else tails[lo] = x; // Same length, lower ending: more room for later numbers.
}
return tails.length; // The right length, though tails itself may not be a subsequence.
}
// Prints 4 4, then 4 4, then 1 1.
for (const nums of [[10, 9, 2, 5, 3, 7, 101, 18], [0, 1, 0, 3, 2, 3], [7, 7, 7, 7, 7, 7, 7]]) {
console.log(lisDp(nums), lisTails(nums));
}// Length of the longest strictly increasing subsequence of nums, two ways.
package main
import "fmt"
// lisDp is the O(n^2) DP: dp[i] = length of the longest increasing subsequence
// ending at nums[i].
func lisDp(nums []int) int {
dp := make([]int, len(nums))
best := 0
for i := range nums {
dp[i] = 1 // Every element alone has length 1.
for j := 0; j < i; j++ {
// nums[i] can extend any increasing subsequence that ends on a smaller value.
if nums[j] < nums[i] {
dp[i] = max(dp[i], dp[j]+1)
}
}
best = max(best, dp[i]) // The longest can end anywhere, not just at the last index.
}
return best
}
// lisTails is the O(n log n) patience version: tails[k] = smallest value that
// ends an increasing subsequence of length k + 1. tails stays sorted, so binary
// search works on it.
func lisTails(nums []int) int {
tails := []int{}
for _, x := range nums {
// Lower bound: the first tail >= x.
lo, hi := 0, len(tails)
for lo < hi {
mid := (lo + hi) / 2
if tails[mid] < x {
lo = mid + 1
} else {
hi = mid
}
}
if lo == len(tails) {
tails = append(tails, x) // x beats every tail: the longest grows by one.
} else {
tails[lo] = x // Same length, lower ending: more room for later numbers.
}
}
return len(tails) // The right length, though tails itself may not be a subsequence.
}
func main() {
// Prints 4 4, then 4 4, then 1 1.
for _, nums := range [][]int{{10, 9, 2, 5, 3, 7, 101, 18}, {0, 1, 0, 3, 2, 3}, {7, 7, 7, 7, 7, 7, 7}} {
fmt.Println(lisDp(nums), lisTails(nums))
}
}using System;
using System.Collections.Generic;
// Length of the longest strictly increasing subsequence of nums, two ways.
public class Solution
{
// O(n^2) DP: dp[i] = length of the longest increasing subsequence ending at nums[i].
public static int LisDp(int[] nums)
{
var dp = new int[nums.Length];
Array.Fill(dp, 1); // Every element alone has length 1.
int best = 0;
for (int i = 0; i < nums.Length; i++)
{
for (int j = 0; j < i; j++)
{
// nums[i] can extend any increasing subsequence that ends on a smaller value.
if (nums[j] < nums[i]) dp[i] = Math.Max(dp[i], dp[j] + 1);
}
best = Math.Max(best, dp[i]); // The longest can end anywhere, not just at the last index.
}
return best;
}
// O(n log n) patience version: tails[k] = smallest value that ends an increasing
// subsequence of length k + 1. tails stays sorted, so binary search works on it.
public static int LisTails(int[] nums)
{
var tails = new List<int>();
foreach (int x in nums)
{
// Lower bound: the first tail >= x.
int lo = 0, hi = tails.Count;
while (lo < hi)
{
int mid = (lo + hi) / 2;
if (tails[mid] < x) lo = mid + 1;
else hi = mid;
}
if (lo == tails.Count) tails.Add(x); // x beats every tail: the longest grows by one.
else tails[lo] = x; // Same length, lower ending: more room for later numbers.
}
return tails.Count; // The right length, though tails itself may not be a subsequence.
}
public static void Main()
{
// Prints 4 4, then 4 4, then 1 1.
var inputs = new[]
{
new[] { 10, 9, 2, 5, 3, 7, 101, 18 },
new[] { 0, 1, 0, 3, 2, 3 },
new[] { 7, 7, 7, 7, 7, 7, 7 },
};
foreach (var nums in inputs)
{
Console.WriteLine($"{LisDp(nums)} {LisTails(nums)}");
}
}
}import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
// Length of the longest strictly increasing subsequence of nums, two ways.
public class Solution {
// O(n^2) DP: dp[i] = length of the longest increasing subsequence ending at nums[i].
static int lisDp(int[] nums) {
int[] dp = new int[nums.length];
Arrays.fill(dp, 1); // Every element alone has length 1.
int best = 0;
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
// nums[i] can extend any increasing subsequence that ends on a smaller value.
if (nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
}
best = Math.max(best, dp[i]); // The longest can end anywhere, not just at the last index.
}
return best;
}
// O(n log n) patience version: tails[k] = smallest value that ends an increasing
// subsequence of length k + 1. tails stays sorted, so binary search works on it.
static int lisTails(int[] nums) {
List<Integer> tails = new ArrayList<>();
for (int x : nums) {
// Lower bound: the first tail >= x.
int lo = 0, hi = tails.size();
while (lo < hi) {
int mid = (lo + hi) / 2;
if (tails.get(mid) < x) lo = mid + 1;
else hi = mid;
}
if (lo == tails.size()) tails.add(x); // x beats every tail: the longest grows by one.
else tails.set(lo, x); // Same length, lower ending: more room for later numbers.
}
return tails.size(); // The right length, though tails itself may not be a subsequence.
}
public static void main(String[] args) {
// Prints 4 4, then 4 4, then 1 1.
int[][] inputs = {{10, 9, 2, 5, 3, 7, 101, 18}, {0, 1, 0, 3, 2, 3}, {7, 7, 7, 7, 7, 7, 7}};
for (int[] nums : inputs) {
System.out.println(lisDp(nums) + " " + lisTails(nums));
}
}
}Common Pitfalls
- Running capacity forwards in the one-row 0/1 knapsack. Each item silently gets reused, and you've solved the unbounded problem instead.
- Skipping the odd-total check in Partition Equal Subset Sum. Integer division rounds the target down, so
[1, 2, 3, 5](total 11) wrongly returnstruebecause2 + 3 = 5. - Returning
dp[n - 1]in the quadratic LIS. The longest subsequence doesn't have to end at the last element: for[1, 2, 0]the answer is 2, not 1. - Using the wrong binary search in the patience version. For strictly increasing, replace the first tail
>= x. Replacing the first tail> xlets equal values extend a subsequence, which finds the longest non-decreasing one instead. - Nesting the loops the wrong way round when counting. With coins outside and amounts inside, Coin Change II counts each combination once. Swap them and
1 + 2and2 + 1count separately, which answers a different question (Combination Sum IV).
Practice
More problems to try on your own.