Back to Home

Dynamic Programming

Dynamic Programming solves complex problems by breaking them down into simpler subproblems, storing solutions to avoid redundant calculations. It's applicable when problems have optimal substructure and overlapping subproblems.

When to Use

  • Problem asks for optimization (maximum, minimum, count)
  • Overlapping subproblems exist
  • Optimal substructure property holds
  • Problems with 'all possible ways' or 'count number of ways'

Common Patterns

1D DP

State depends on previous states in a linear fashion

Example: Fibonacci, house robber, climbing stairs

2D DP

State depends on two parameters

Example: Longest common subsequence, edit distance, unique paths

Knapsack

Choose items with constraints to optimize value

Example: 0/1 knapsack, coin change, partition problems

Code Templates

Python

# 1D DP template
def dp_1d(n):
    dp = [0] * (n + 1)
    # Base cases
    dp[0] = base_value_0
    dp[1] = base_value_1
    
    for i in range(2, n + 1):
        dp[i] = transition_function(dp, i)
    
    return dp[n]

# 2D DP template
def dp_2d(s1, s2):
    m, n = len(s1), len(s2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    
    # Base cases
    for i in range(m + 1):
        dp[i][0] = base_value_i
    for j in range(n + 1):
        dp[0][j] = base_value_j
    
    # Fill DP table
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if condition(s1[i-1], s2[j-1]):
                dp[i][j] = transition_match(dp, i, j)
            else:
                dp[i][j] = transition_no_match(dp, i, j)
    
    return dp[m][n]

# 0/1 Knapsack template
def knapsack(weights, values, capacity):
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]
    
    for i in range(1, n + 1):
        for w in range(capacity + 1):
            # Don't take item i-1
            dp[i][w] = dp[i-1][w]
            
            # Take item i-1 if possible
            if weights[i-1] <= w:
                dp[i][w] = max(dp[i][w], 
                              dp[i-1][w - weights[i-1]] + values[i-1])
    
    return dp[n][capacity]

C++

// 1D DP template
int dp1D(int n) {
    vector<int> dp(n + 1, 0);
    // Base cases
    dp[0] = baseValue0;
    dp[1] = baseValue1;
    
    for (int i = 2; i <= n; i++) {
        dp[i] = transitionFunction(dp, i);
    }
    
    return dp[n];
}

// 2D DP template
int dp2D(string s1, string s2) {
    int m = s1.length(), n = s2.length();
    vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
    
    // Base cases
    for (int i = 0; i <= m; i++) dp[i][0] = baseValueI;
    for (int j = 0; j <= n; j++) dp[0][j] = baseValueJ;
    
    // Fill DP table
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            if (condition(s1[i-1], s2[j-1])) {
                dp[i][j] = transitionMatch(dp, i, j);
            } else {
                dp[i][j] = transitionNoMatch(dp, i, j);
            }
        }
    }
    
    return dp[m][n];
}

// 0/1 Knapsack template
int knapsack(vector<int>& weights, vector<int>& values, int capacity) {
    int n = weights.size();
    vector<vector<int>> dp(n + 1, vector<int>(capacity + 1, 0));
    
    for (int i = 1; i <= n; i++) {
        for (int w = 0; w <= capacity; w++) {
            // Don't take item i-1
            dp[i][w] = dp[i-1][w];
            
            // Take item i-1 if possible
            if (weights[i-1] <= w) {
                dp[i][w] = max(dp[i][w], 
                              dp[i-1][w - weights[i-1]] + values[i-1]);
            }
        }
    }
    
    return dp[n][capacity];
}

Practice Problems

Common Pitfalls

  • ⚠️Incorrect base case initialization
  • ⚠️Wrong order of iteration (depends on dependencies)
  • ⚠️Not considering all possible transitions
  • ⚠️Off-by-one errors in array indexing
  • ⚠️Forgetting to handle edge cases like empty inputs

Complexity

Time
Typically O(n²) or O(n·m) for 2D problems
Space
O(n) or O(n·m), often optimizable to O(n) or O(1)

Much better than exponential brute force

Prerequisites