Stop Recomputing, Start Remembering Master Dynamic Programming From Knapsack to Tree DP & Digit DP — Identify States, Build Transitions, Optimize Space

|
0 Topics
0 Core Patterns
0 Exercises

🧮 Learn Dynamic Programming

Identify overlapping subproblems, define states cleanly, and convert recursion into efficient tabulated solutions.

0 / 6 topics completed
0%
00

Introduction to Dynamic Programming

Dynamic Programming (DP) is an optimization technique for problems that have two key properties: optimal substructure (the optimal solution to a problem can be built from optimal solutions of its subproblems) and overlapping subproblems (the same subproblems are solved repeatedly in a naive recursive solution).

💡

The core DP workflow is always the same three steps: write the brute-force recursion first, identify which calls repeat and add memoization (top-down caching), then convert to tabulation (bottom-up, iterative) for the best performance and to eliminate stack overhead.

Plain Recursion
Re-solves same states Often exponential time
Memoization (Top-Down)
Cache results by state Recursion + cache = O(states)
Tabulation (Bottom-Up)
Iterative, fills a table No call stack overhead

How to Identify a DP State

Ask: "What changes between recursive calls?"
Ask: "What's the minimum info needed to describe a subproblem?"
Result: Those changing variables become your DP state — usually an index plus 1-2 constraints
01

0/1 Knapsack

Given n items with weights and values, and a knapsack of capacity W, choose a subset of items maximizing total value without exceeding W. Each item can be picked at most once (hence "0/1" — take it or leave it).

Identifying the State

State:dp[i][w] = max value using first i items with capacity w
Transition:dp[i][w] = max(dp[i-1][w], val[i] + dp[i-1][w-wt[i]])
Base case:dp[0][w] = 0 for all w (no items = no value)

Tabulation (Bottom-Up)

Python
def knapsack_tab(wt, val, W, n):
    dp = [[0] * (W + 1) for _ in range(n + 1)]
    for i in range(1, n + 1):
        for w in range(W + 1):
            if wt[i-1] <= w:
                dp[i][w] = max(dp[i-1][w], val[i-1] + dp[i-1][w-wt[i-1]])
            else:
                dp[i][w] = dp[i-1][w]
    return dp[n][W]
C++
int knapsackTab(vector<int>& wt, vector<int>& val, int W, int n) {
    vector<vector<int>> dp(n+1, vector<int>(W+1, 0));
    for (int i = 1; i <= n; i++) {
        for (int w = 0; w <= W; w++) {
            if (wt[i-1] <= w) {
                dp[i][w] = max(dp[i-1][w], val[i-1] + dp[i-1][w-wt[i-1]]);
            } else {
                dp[i][w] = dp[i-1][w];
            }
        }
    }
    return dp[n][W];
}
02

Unbounded Knapsack

Same setup as 0/1 Knapsack, but now each item can be used unlimited times. This is the same pattern behind "Coin Change" and "Rod Cutting" problems.

The Key Difference from 0/1 Knapsack

0/1: dp[i-1][w-wt[i-1]] — move to previous item after taking it
Unbounded: dp[i][w-wt[i-1]] — stay on the SAME item, since it can be reused
Python
def unbounded_knapsack(wt, val, W, n):
    dp = [0] * (W + 1)
    for w in range(1, W + 1):
        for i in range(n):
            if wt[i] <= w:
                dp[w] = max(dp[w], val[i] + dp[w - wt[i]])
    return dp[W]
C++
int unboundedKnapsack(vector<int>& wt, vector<int>& val, int W, int n) {
    vector<int> dp(W+1, 0);
    for (int w = 1; w <= W; w++) {
        for (int i = 0; i < n; i++) {
            if (wt[i] <= w) {
                dp[w] = max(dp[w], val[i] + dp[w - wt[i]]);
            }
        }
    }
    return dp[W];
}
03

Longest Increasing Subsequence (LIS)

Given an array, find the length of the longest subsequence (not necessarily contiguous) where every element is strictly greater than the previous one.

🚀

O(N log N) Optimization: Maintain a tails array where tails[k] = smallest possible tail value of an increasing subsequence of length k+1. For each new number, binary search for its position in tails and replace it. The length of tails at the end is the LIS length.

Python
import bisect
def lis_nlogn(arr):
    tails = []
    for num in arr:
        pos = bisect.bisect_left(tails, num)
        if pos == len(tails):
            tails.append(num)
        else:
            tails[pos] = num
    return len(tails)
C++
int lisNLogN(vector<int>& arr) {
    vector<int> tails;
    for (int num : arr) {
        auto it = lower_bound(tails.begin(), tails.end(), num);
        if (it == tails.end()) {
            tails.push_back(num);
        } else {
            *it = num;
        }
    }
    return tails.size();
}
04

Longest Common Subsequence (LCS)

Given two strings, find the length of their longest common subsequence. A subsequence is formed by deleting some characters without changing the order of remaining characters.

Identifying the State

State: dp[i][j] = LCS of string A[0..i-1] and string B[0..j-1]
Match: If A[i-1] == B[j-1]: dp[i][j] = dp[i-1][j-1] + 1
No Match: If A[i-1] != B[j-1]: dp[i][j] = max(dp[i-1][j], dp[i][j-1])
Python
def longest_common_subsequence(text1, text2):
    n, m = len(text1), len(text2)
    dp = [[0] * (m + 1) for _ in range(n + 1)]
    
    for i in range(1, n + 1):
        for j in range(1, m + 1):
            if text1[i-1] == text2[j-1]:
                dp[i][j] = dp[i-1][j-1] + 1
            else:
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])
                
    return dp[n][m]
C++
int longestCommonSubsequence(string text1, text2) {
    int n = text1.length(), m = text2.length();
    vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
    
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (text1[i-1] == text2[j-1]) {
                dp[i][j] = dp[i-1][j-1] + 1;
            } else {
                dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
            }
        }
    }
    return dp[n][m];
}
05

Tree DP

Dynamic Programming on Trees involves traversing the tree (usually using DFS) and solving the subproblems defined at each node based on the solutions of its children.

🌳

Common Pattern: At node `u`, compute an answer assuming `u` is included in the solution, and compute another answer assuming `u` is excluded. Return both to the parent.

06

Digit DP

Digit DP is used for problems that ask for the count or sum of numbers in a given range [L, R] that satisfy a specific property (e.g., sum of digits is a prime number).

Standard State Template

Parameters: dp(idx, tight, sum)
idx: Current position from left to right
tight: Boolean. If true, the upper bound for the current digit is limited by the original number.

Practice Exercises

1. Climbing Stairs

You are climbing a staircase. It takes n steps to reach the top. Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?

Hint: This is perfectly equivalent to the Fibonacci sequence. The number of ways to reach step i is the sum of ways to reach step i-1 and step i-2.

Python
def climbStairs(n):
    if n <= 2: return n
    a, b = 1, 2
    for _ in range(3, n + 1):
        a, b = b, a + b
    return b
2. House Robber

You are a professional robber planning to rob houses along a street. You cannot rob adjacent houses. Given an integer array nums representing the money in each house, return the maximum amount of money you can rob.

Hint: For each house i, you either rob it (money[i] + max from i-2) or skip it (max from i-1).

C++
int rob(vector<int>& nums) {
    int rob1 = 0, rob2 = 0;
    for (int n : nums) {
        int temp = max(n + rob1, rob2);
        rob1 = rob2;
        rob2 = temp;
    }
    return rob2;
}