Dynamic Programming Pattern

Dynamic Programming (DP) is a method for solving complex problems by breaking them down into simpler subproblems. It stores the results of subproblems to avoid recomputing them, making it highly efficient for problems with overlapping subproblems and optimal substructure.

When to Use This Pattern

  • Problems with overlapping subproblems
  • Problems with optimal substructure (optimal solution contains optimal solutions to subproblems)
  • Optimization problems (maximize or minimize something)
  • Counting problems (how many ways to...)
  • Problems that can be solved recursively but are inefficient

Key Concepts

1. Overlapping Subproblems

The problem can be broken down into subproblems which are reused several times. For example, in Fibonacci, F(5) = F(4) + F(3), and F(4) = F(3) + F(2), so F(3) is computed multiple times.

2. Optimal Substructure

An optimal solution to the problem contains optimal solutions to subproblems. For example, the shortest path from A to C through B is the shortest path from A to B plus the shortest path from B to C.

3. Two Approaches

  • Top-Down (Memoization): Start with the main problem and recursively solve subproblems, storing results in a memo table.
  • Bottom-Up (Tabulation): Start with the smallest subproblems and build up to the main problem using a table.

Visual Representation - Fibonacci DP

Dynamic Programming - Fibonacci Sequence Comparison

Without DP (Recursive): O(2^n) F(5) F(4) F(3) F(3) F(2) ⚠️ F(3) calculated twice! Exponential time: O(2^n) With DP (Bottom-Up): O(n) 0 F(0) 1 F(1) 1 F(2) 2 F(3) 3 F(4) 5 F(5) ✓ F(2)=1+0 F(3)=1+1 F(4)=2+1 F(5)=3+2 ✓ Each value calculated once and reused! DP Table Construction (Bottom-Up) Base Cases: dp[0] = 0, dp[1] = 1 Recurrence Relation: dp[i] = dp[i-1] + dp[i-2] for i ≥ 2 Step-by-Step: dp[2] = dp[1] + dp[0] = 1 + 0 = 1 dp[3] = dp[2] + dp[1] = 1 + 1 = 2 dp[4] = dp[3] + dp[2] = 2 + 1 = 3 dp[5] = dp[4] + dp[3] = 3 + 2 = 5 Time: O(n) | Space: O(n) | No redundant calculations!

Key Insight: DP stores results of subproblems to avoid recomputation. This transforms exponential O(2^n) time to linear O(n) time, making it feasible to compute large Fibonacci numbers.

Common DP Patterns

  • Fibonacci Numbers: F(n) = F(n-1) + F(n-2)
  • 0/1 Knapsack: Choose items with weight and value constraints
  • Longest Common Subsequence (LCS): Find longest common subsequence between two strings
  • Longest Increasing Subsequence (LIS): Find longest increasing subsequence
  • Subset Sum: Check if a subset sums to a target

Algorithm Section

Top-Down Approach (Memoization)

function dp_topdown(n, memo):
    if n in memo:
        return memo[n]
    
    if base_case(n):
        result = base_value
    else:
        result = combine(dp_topdown(subproblem1, memo),
                         dp_topdown(subproblem2, memo))
    
    memo[n] = result
    return result

Bottom-Up Approach (Tabulation)

function dp_bottomup(n):
    dp = array of size n+1
    dp[0] = base_value
    
    for i from 1 to n:
        dp[i] = combine(dp[subproblem1], dp[subproblem2])
    
    return dp[n]

Fibonacci Example in C++

#include <vector>
#include <unordered_map>
using namespace std;

// Top-Down (Memoization)
unordered_map<int, int> memo;

int fib_memo(int n) {
    if (memo.find(n) != memo.end()) {
        return memo[n];
    }
    if (n <= 1) {
        return n;
    }
    memo[n] = fib_memo(n - 1) + fib_memo(n - 2);
    return memo[n];
}

// Bottom-Up (Tabulation)
int fib_tab(int n) {
    if (n <= 1) {
        return n;
    }
    vector<int> dp(n + 1, 0);
    dp[1] = 1;
    for (int i = 2; i <= n; i++) {
        dp[i] = dp[i-1] + dp[i-2];
    }
    return dp[n];
}

0/1 Knapsack Pattern in C++

#include <vector>
#include <algorithm>
using namespace std;

int knapsack(vector<int>& weights, vector<int>& values, int capacity) {
    int n = weights.size();
    // dp[i][w] = maximum value with first i items and capacity w
    vector<vector<int>> dp(n + 1, vector<int>(capacity + 1, 0));
    
    for (int i = 1; i <= n; i++) {
        for (int w = 1; w <= capacity; w++) {
            if (weights[i-1] <= w) {
                // Choose max of: don't take item, or take item
                dp[i][w] = max(
                    dp[i-1][w],  // Don't take item i
                    dp[i-1][w - weights[i-1]] + values[i-1]  // Take item i
                );
            } else {
                // Can't take item i (too heavy)
                dp[i][w] = dp[i-1][w];
            }
        }
    }
    
    return dp[n][capacity];
}

Practice Problems

Problem 1: Climbing Stairs (LeetCode #70)

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?

Example:

Input: n = 3
Output: 3
Explanation: There are three ways: 1+1+1, 1+2, 2+1

Solution in C++:

#include <vector>
using namespace std;

int climbStairs(int n) {
    if (n <= 2) {
        return n;
    }
    
    vector<int> dp(n + 1, 0);
    dp[1] = 1;
    dp[2] = 2;
    
    for (int i = 3; i <= n; i++) {
        dp[i] = dp[i-1] + dp[i-2];
    }
    
    return dp[n];
}

// Space-optimized version
int climbStairs_optimized(int n) {
    if (n <= 2) {
        return n;
    }
    
    int prev2 = 1;  // ways to reach step 1
    int prev1 = 2;  // ways to reach step 2
    
    for (int i = 3; i <= n; i++) {
        int current = prev1 + prev2;
        prev2 = prev1;
        prev1 = current;
    }
    
    return prev1;
}

Step-by-Step Simulation (n=5):

Base Cases:

dp[1] = 1 (only way: 1 step)

dp[2] = 2 (ways: 1+1 or 2)

Building DP table:

dp[3] = dp[2] + dp[1] = 2 + 1 = 3

dp[4] = dp[3] + dp[2] = 3 + 2 = 5

dp[5] = dp[4] + dp[3] = 5 + 3 = 8

Result: 8 ways to reach step 5

Explanation: This is essentially a Fibonacci problem. To reach step n, you can come from step n-1 (1 step) or step n-2 (2 steps). So ways(n) = ways(n-1) + ways(n-2). We use bottom-up DP to build the solution. The space-optimized version only keeps track of the last two values, reducing space from O(n) to O(1).

Time Complexity: O(n)

Space Complexity: O(n) or O(1) optimized

Problem 2: House Robber (LeetCode #198)

You are a robber planning to rob houses along a street. Each house has a certain amount of money. You cannot rob two adjacent houses. Determine the maximum amount of money you can rob.

Example:

Input: nums = [2,7,9,3,1]
Output: 12
Explanation: Rob house 1 (money = 2), rob house 3 (money = 9), rob house 5 (money = 1).

Solution in C++:

#include <vector>
#include <algorithm>
using namespace std;

int rob(vector<int>& nums) {
    if (nums.empty()) {
        return 0;
    }
    if (nums.size() == 1) {
        return nums[0];
    }
    
    vector<int> dp(nums.size(), 0);
    dp[0] = nums[0];
    dp[1] = max(nums[0], nums[1]);
    
    for (int i = 2; i < nums.size(); i++) {
        // Either skip house i (take dp[i-1])
        // Or rob house i (take dp[i-2] + nums[i])
        dp[i] = max(dp[i-1], dp[i-2] + nums[i]);
    }
    
    return dp[nums.size() - 1];
}

// Space-optimized version
int rob_optimized(vector<int>& nums) {
    int prev2 = 0;  // max money up to i-2
    int prev1 = 0;  // max money up to i-1
    
    for (int num : nums) {
        int current = max(prev1, prev2 + num);
        prev2 = prev1;
        prev1 = current;
    }
    
    return prev1;
}

Step-by-Step Simulation:

Input: nums = [2, 7, 9, 3, 1]

Base Cases:

dp[0] = 2 (rob house 0)

dp[1] = max(2, 7) = 7 (rob house 1, skip house 0)

Building DP:

dp[2] = max(dp[1], dp[0] + 9) = max(7, 2+9) = 11

dp[3] = max(dp[2], dp[1] + 3) = max(11, 7+3) = 11

dp[4] = max(dp[3], dp[2] + 1) = max(11, 11+1) = 12

Result: 12 (rob houses 0, 2, 4)

Explanation: At each house, we have two choices: rob it or skip it. If we rob house i, we can't rob house i-1, so we take dp[i-2] + nums[i]. If we skip it, we take dp[i-1]. We choose the maximum. The recurrence is: dp[i] = max(dp[i-1], dp[i-2] + nums[i]). This ensures we never rob two adjacent houses while maximizing the total money.

Time Complexity: O(n)

Space Complexity: O(n) or O(1) optimized

Problem 3: Coin Change (LeetCode #322)

You are given an integer array coins representing coins of different denominations and an integer amount representing a total amount of money. Return the fewest number of coins that you need to make up that amount.

Example:

Input: coins = [1,2,5], amount = 11
Output: 3
Explanation: 11 = 5 + 5 + 1

Solution in C++:

#include <vector>
#include <algorithm>
#include <climits>
using namespace std;

int coinChange(vector<int>& coins, int amount) {
    // dp[i] = minimum coins needed to make amount i
    vector<int> dp(amount + 1, INT_MAX);
    dp[0] = 0;  // 0 coins needed for amount 0
    
    // For each coin, update all amounts >= coin
    for (int coin : coins) {
        for (int i = coin; i <= amount; i++) {
            if (dp[i - coin] != INT_MAX) {
                // Use this coin: dp[i] = min(current, 1 + dp[i-coin])
                dp[i] = min(dp[i], dp[i - coin] + 1);
            }
        }
    }
    
    return (dp[amount] == INT_MAX) ? -1 : dp[amount];
}

Step-by-Step Simulation:

Input: coins = [1, 2, 5], amount = 11

Initial: dp = [0, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞]

Processing coin 1:

dp[1] = min(∞, 1 + dp[0]) = 1

dp[2] = min(∞, 1 + dp[1]) = 2

... (all amounts become their value)

Processing coin 2:

dp[2] = min(2, 1 + dp[0]) = 1

dp[4] = min(4, 1 + dp[2]) = 2

Processing coin 5:

dp[5] = min(5, 1 + dp[0]) = 1

dp[10] = min(10, 1 + dp[5]) = 2

dp[11] = min(11, 1 + dp[6]) = 3

Result: 3 (5 + 5 + 1)

Explanation: We use bottom-up DP. dp[i] represents the minimum number of coins needed to make amount i. For each coin, we update dp[i] by checking if using this coin gives us a better solution: dp[i] = min(dp[i], dp[i-coin] + 1). We initialize dp[0] = 0 (0 coins for amount 0) and all others as infinity. This ensures we find the optimal solution by trying all coin combinations.

Time Complexity: O(amount × len(coins))

Space Complexity: O(amount)

Problem 4: Longest Common Subsequence (LeetCode #1143)

Given two strings text1 and text2, return the length of their longest common subsequence.

Example:

Input: text1 = "abcde", text2 = "ace"
Output: 3
Explanation: The longest common subsequence is "ace" and its length is 3.

Solution in C++:

#include <vector>
#include <string>
#include <algorithm>
using namespace std;

int longestCommonSubsequence(string text1, string text2) {
    int m = text1.length();
    int n = text2.length();
    
    // dp[i][j] = LCS of text1[0..i-1] and text2[0..j-1]
    vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
    
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (text1[i-1] == text2[j-1]) {
                // Characters match: extend LCS
                dp[i][j] = dp[i-1][j-1] + 1;
            } else {
                // Characters don't match: take max of skipping either
                dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
            }
        }
    }
    
    return dp[m][n];
}

Step-by-Step Simulation:

Input: text1 = "abcde", text2 = "ace"

DP Table (showing key steps):

i=1, j=1: 'a' == 'a' → dp[1][1] = dp[0][0] + 1 = 1

i=1, j=2: 'a' != 'c' → dp[1][2] = max(dp[0][2], dp[1][1]) = 1

i=2, j=2: 'b' != 'c' → dp[2][2] = max(dp[1][2], dp[2][1]) = 1

i=3, j=2: 'c' == 'c' → dp[3][2] = dp[2][1] + 1 = 2

i=4, j=3: 'd' != 'e' → dp[4][3] = max(dp[3][3], dp[4][2]) = 2

i=5, j=3: 'e' == 'e' → dp[5][3] = dp[4][2] + 1 = 3

Result: 3 (LCS = "ace")

Explanation: We use a 2D DP table where dp[i][j] represents the LCS of text1[0:i] and text2[0:j]. If characters match, we take dp[i-1][j-1] + 1 (extend the LCS). Otherwise, we take the maximum of either skipping character from text1 (dp[i-1][j]) or from text2 (dp[i][j-1]). This ensures we find the longest common subsequence by considering all possibilities.

Time Complexity: O(m × n)

Space Complexity: O(m × n)

Problem 5: Longest Increasing Subsequence (LeetCode #300)

Given an integer array nums, return the length of the longest strictly increasing subsequence.

Example:

Input: nums = [10,9,2,5,3,7,101,18]
Output: 4
Explanation: The longest increasing subsequence is [2,3,7,18], therefore the length is 4.

Solution in C++:

#include <vector>
#include <algorithm>
using namespace std;

// O(n²) approach
int lengthOfLIS(vector<int>& nums) {
    int n = nums.size();
    // dp[i] = length of LIS ending at index i
    vector<int> dp(n, 1);
    
    for (int i = 1; i < n; i++) {
        for (int j = 0; j < i; j++) {
            if (nums[j] < nums[i]) {
                // Can extend subsequence ending at j
                dp[i] = max(dp[i], dp[j] + 1);
            }
        }
    }
    
    return *max_element(dp.begin(), dp.end());
}

// O(n log n) optimized approach using binary search
int lengthOfLIS_optimized(vector<int>& nums) {
    vector<int> tails;  // tails[i] = smallest tail of LIS of length i+1
    
    for (int num : nums) {
        // Binary search for the position to replace
        auto it = lower_bound(tails.begin(), tails.end(), num);
        
        if (it == tails.end()) {
            // num is larger than all tails, extend LIS
            tails.push_back(num);
        } else {
            // Replace first element >= num
            *it = num;
        }
    }
    
    return tails.size();
}

Step-by-Step Simulation:

Input: nums = [10, 9, 2, 5, 3, 7, 101, 18]

DP Approach:

dp[0] = 1 (LIS ending at 0: [10])

dp[1] = 1 (LIS ending at 1: [9])

dp[2] = 1 (LIS ending at 2: [2])

dp[3] = max(1, dp[2]+1) = 2 (LIS: [2,5])

dp[4] = max(1, dp[2]+1) = 2 (LIS: [2,3])

dp[5] = max(1, dp[3]+1, dp[4]+1) = 3 (LIS: [2,5,7] or [2,3,7])

dp[6] = max(1, dp[5]+1) = 4 (LIS: [2,5,7,101])

dp[7] = max(1, dp[5]+1) = 4 (LIS: [2,5,7,18])

Result: 4

Explanation: DP approach: dp[i] is the length of LIS ending at index i. For each position, we check all previous positions and if nums[j] < nums[i], we can extend the subsequence. The optimized version uses binary search to maintain the smallest tail element for each LIS length, achieving O(n log n) time. The key insight is that we only need to track the smallest tail for each length to maximize future extension possibilities.

Time Complexity: O(n²) or O(n log n) optimized

Space Complexity: O(n)