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.

AdvancedRead first: Dynamic Programming: 1D

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
  1. 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.
  2. With no items there's nothing to carry, so row 0 is all 0. Every later row is built from the row above it.
  3. Item (1, 6) weighs 1, so it doesn't fit at capacity 0. That cell can only skip it and copy the best value from the row above.
  4. dp[1][1]: skip keeps 0 from above. Take adds 6 to dp[0][0], the best that fits in the 0 units left. Taking wins: 0 + 6 = 6 beats 0. Take reads row 0, from before this item, so it can't be used twice.
  5. Capacities 2 to 5 face the same choice as 1: skip 0 or take 6, so each is 6. Every item so far already fits, so the extra room has nothing to add.
  6. Item (2, 10) weighs 2, so it doesn't fit at capacity 0 to 1. Those cells can only skip it and copy the best value from the row above.
  7. dp[2][2]: skip keeps 6 from above. Take adds 10 to dp[1][0], the best that fits in the 0 units left. Taking wins: 0 + 10 = 10 beats 6.
  8. dp[2][3]: skip keeps 6 from above. Take adds 10 to dp[1][1], the best that fits in the 1 unit left. Taking wins: 6 + 10 = 16 beats 6. That packs (2, 10) on top of earlier items worth 6.
  9. Capacities 4 to 5 face the same choice as 3: skip 6 or take 16, so each is 16. Every item so far already fits, so the extra room has nothing to add.
  10. Item (3, 12) weighs 3, so it doesn't fit at capacity 0 to 2. Those cells can only skip it and copy the best value from the row above.
  11. dp[3][3]: skip keeps 16 from above. Take adds 12 to dp[2][0], the best that fits in the 0 units left. Skipping wins: 16 beats 0 + 12 = 12.
  12. dp[3][4]: skip keeps 16 from above. Take adds 12 to dp[2][1], the best that fits in the 1 unit left. Taking wins: 6 + 12 = 18 beats 16. That packs (3, 12) on top of earlier items worth 6.
  13. dp[3][5]: skip keeps 16 from above. Take adds 12 to dp[2][2], the best that fits in the 2 units left. Taking wins: 10 + 12 = 22 beats 16. That packs (3, 12) on top of earlier items worth 10.
  14. dp[3][5] = 22 is the answer: the best value from all 3 items within capacity 5. Each of the 24 cells 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 max becomes OR. Partition Equal Subset Sum, below.
  • Counting. How many ways reach a total, so max becomes +.

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

OperationTimeSpace
0/1 knapsack, 2D table (n items, capacity C)O(n · C)O(n · C)
0/1 knapsack, one rowO(n · C)O(C)
Subset sum to SO(n · S)O(S)
Longest Increasing Subsequence, quadratic DPO(n²)O(n)
Longest Increasing Subsequence, patienceO(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)
Each row adds one item. Both cells that (22) reads are in the row above it.

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
Forwards, dp[4] reads a dp[2] that already includes the item, so the item gets taken twice for 20.

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.

Partition Equal Subset Sum

MEDIUMLeetCode 416

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 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
T marks a reachable sum after each number. dp[11] turns true when 11 arrives, so the answer is true.

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)
    }
}

Longest Increasing Subsequence

MEDIUMLeetCode 300

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 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
The final length of tails, 4, is the answer.

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 returns true because 2 + 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 > x lets 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 + 2 and 2 + 1 count separately, which answers a different question (Combination Sum IV).

Practice

More problems to try on your own.