Dynamic Programming

Solve complex problems by breaking them down into simpler subproblems, with real-world examples and patterns.


Dynamic Programming (DP) is a powerful problem-solving technique that breaks down complex problems into simpler overlapping subproblems and stores their solutions to avoid redundant calculations. Let's explore the core concepts, common patterns, and real-world applications of DP.

1. Core Concepts

Dynamic Programming works by following these key principles:

Optimal Substructure

A problem has optimal substructure if an optimal solution can be constructed from optimal solutions of its subproblems.

Overlapping Subproblems

The same subproblems are solved multiple times when a recursive solution is applied.

Memoization vs. Tabulation

  • Memoization (Top-Down): Recursive approach with results stored in a memo table
  • Tabulation (Bottom-Up): Iterative approach building up solutions from the smallest subproblems

2. Fibonacci Sequence: A Classic Example

The Fibonacci sequence is a perfect introduction to dynamic programming.

Naive Recursive Approach (Exponential Time)

int fibonacci(int n) {
    if (n <= 1) return n;
    return fibonacci(n-1) + fibonacci(n-2);
}

Top-Down with Memoization (Linear Time)

int fibonacciMemo(int n, vector<int>& memo) {
    if (n <= 1) return n;
 
    // If we've already computed this value, return it
    if (memo[n] != -1) return memo[n];
 
    // Compute and store the result
    memo[n] = fibonacciMemo(n-1, memo) + fibonacciMemo(n-2, memo);
    return memo[n];
}
 
int fibonacci(int n) {
    vector<int> memo(n+1, -1);
    return fibonacciMemo(n, memo);
}

Bottom-Up with Tabulation (Linear Time)

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

Space-Optimized Version (Constant Space)

int fibonacci(int n) {
    if (n <= 1) return n;
 
    int prev = 0;
    int curr = 1;
 
    for (int i = 2; i <= n; i++) {
        int next = prev + curr;
        prev = curr;
        curr = next;
    }
 
    return curr;
}

3. Common Dynamic Programming Patterns

1. Linear Sequence DP

Problems where the state depends on the previous elements in a sequence.

Example: Maximum Subarray Sum (Kadane's Algorithm)

Find the contiguous subarray with the largest sum.

int maxSubarraySum(vector<int>& nums) {
    int currentMax = nums[0];
    int globalMax = nums[0];
 
    for (int i = 1; i < nums.size(); i++) {
        // Either extend the previous subarray or start a new one
        currentMax = max(nums[i], currentMax + nums[i]);
        globalMax = max(globalMax, currentMax);
    }
 
    return globalMax;
}

2. Grid-Based DP

Problems involving traversal or computation on a 2D grid.

Example: Minimum Path Sum

Find the path with the minimum sum from top-left to bottom-right in a grid.

int minPathSum(vector<vector<int>>& grid) {
    int m = grid.size();
    int n = grid[0].size();
 
    // Initialize dp table
    vector<vector<int>> dp(m, vector<int>(n));
    dp[0][0] = grid[0][0];
 
    // Fill first row
    for (int j = 1; j < n; j++)
        dp[0][j] = dp[0][j-1] + grid[0][j];
 
    // Fill first column
    for (int i = 1; i < m; i++)
        dp[i][0] = dp[i-1][0] + grid[i][0];
 
    // Fill rest of the dp table
    for (int i = 1; i < m; i++) {
        for (int j = 1; j < n; j++) {
            dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1]);
        }
    }
 
    return dp[m-1][n-1];
}

3. String DP Problems

Problems involving string operations, matching, or transformations.

Example: Longest Common Subsequence

Find the length of the longest subsequence common to two strings.

int longestCommonSubsequence(string text1, string text2) {
    int m = text1.length();
    int n = text2.length();
 
    // Create a DP table
    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])
                dp[i][j] = 1 + dp[i-1][j-1];
            else
                dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
        }
    }
 
    return dp[m][n];
}

4. Decision Making DP

Problems involving decisions that affect future states and outcomes.

Example: House Robber

Determine the maximum amount of money that can be robbed without alerting the police (can't rob adjacent houses).

int rob(vector<int>& nums) {
    int n = nums.size();
    if (n == 0) return 0;
    if (n == 1) return nums[0];
 
    vector<int> dp(n);
    dp[0] = nums[0];
    dp[1] = max(nums[0], nums[1]);
 
    for (int i = 2; i < n; i++) {
        // Either rob current house and skip previous one, or skip current house
        dp[i] = max(dp[i-2] + nums[i], dp[i-1]);
    }
 
    return dp[n-1];
}

5. Knapsack-Type Problems

Problems involving selection from a set of items with constraints.

Example: 0/1 Knapsack Problem

Select items with weights and values to maximize value while staying within a weight limit.

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 = 1; w <= capacity; w++) {
            // If current item can fit
            if (weights[i-1] <= w)
                // Max of: including current item or excluding it
                dp[i][w] = max(values[i-1] + dp[i-1][w-weights[i-1]], dp[i-1][w]);
            else
                // Can't include current item
                dp[i][w] = dp[i-1][w];
        }
    }
 
    return dp[n][capacity];
}

4. Real-World Applications

Dynamic Programming is used in many practical applications:

1. Shortest Path Algorithms

The Floyd-Warshall algorithm uses DP to find the shortest paths between all pairs of vertices in a weighted graph.

void floydWarshall(vector<vector<int>>& graph, int V) {
    vector<vector<int>> dist = graph;
 
    // Consider each vertex as an intermediate
    for (int k = 0; k < V; k++) {
        // Pick all vertices as source
        for (int i = 0; i < V; i++) {
            // Pick all vertices as destination
            for (int j = 0; j < V; j++) {
                // If vertex k is on the shortest path from i to j,
                // update the value of dist[i][j]
                if (dist[i][k] != INF && dist[k][j] != INF &&
                    dist[i][k] + dist[k][j] < dist[i][j])
                    dist[i][j] = dist[i][k] + dist[k][j];
            }
        }
    }
 
    // Print the shortest distance matrix
    printSolution(dist, V);
}

2. Resource Allocation

DP helps optimize resource allocation in systems with constrained resources.

3. Text Processing and Bioinformatics

Algorithms like sequence alignment in DNA/protein sequences use DP.

4. Financial Planning and Investment Strategies

Portfolio optimization uses DP to maximize returns while managing risk.

5. Speech Recognition

Hidden Markov Models with DP algorithms like Viterbi are used in speech recognition systems.

5. Advanced Techniques

State Compression

When the state space is large, we can compress the state representation using bit manipulation.

Example: Travelling Salesman Problem with Bitmasks

int tsp(vector<vector<int>>& dist, int mask, int pos, vector<vector<int>>& dp) {
    int n = dist.size();
 
    // If all cities have been visited
    if (mask == ((1 << n) - 1))
        return dist[pos][0];
 
    // If this state has been computed before
    if (dp[mask][pos] != -1)
        return dp[mask][pos];
 
    int ans = INT_MAX;
 
    // Try to go to an unvisited city
    for (int city = 0; city < n; city++) {
        if ((mask & (1 << city)) == 0) {
            int newAns = dist[pos][city] + tsp(dist, mask | (1 << city), city, dp);
            ans = min(ans, newAns);
        }
    }
 
    return dp[mask][pos] = ans;
}

Divide and Conquer DP

Some problems can be solved more efficiently using a divide-and-conquer approach with DP.

Multi-dimensional DP

Some problems require multiple dimensions to represent the state.

Example: Coin Change 2 (Number of Ways)

int change(int amount, vector<int>& coins) {
    vector<int> dp(amount + 1, 0);
    dp[0] = 1;
 
    for (int coin : coins) {
        for (int i = coin; i <= amount; i++) {
            dp[i] += dp[i - coin];
        }
    }
 
    return dp[amount];
}

6. DP Problem-Solving Framework

When approaching a DP problem, follow these steps:

  1. Identify the problem state

    • What information do we need to make a decision at each step?
  2. Define the recurrence relation

    • How does the current state relate to previous states?
  3. Identify the base cases

    • What are the simplest subproblems that we know the answers to?
  4. Decide on a DP approach

    • Top-down (memoization) or bottom-up (tabulation)?
  5. Optimize if needed

    • Can we reduce the space complexity?

7. Common DP Problem Patterns

1. Minimum/Maximum Path to Reach a Target

  • Examples: Minimum Path Sum, Coin Change, Longest Increasing Subsequence

2. Distinct Ways to Reach a Target

  • Examples: Climbing Stairs, Unique Paths, Coin Change 2

3. Merging Intervals

  • Examples: Burst Balloons, Minimum Cost Tree From Leaf Values

4. Decision Making

  • Examples: House Robber, Best Time to Buy and Sell Stock with Cooldown

5. String Matching and Transformation

  • Examples: Edit Distance, Regular Expression Matching

8. Time and Space Complexity Analysis

The time complexity of a DP solution typically depends on:

  • Number of states (size of the DP table)
  • Work done per state

For example, in the Knapsack problem:

  • Number of states: O(n * capacity)
  • Work per state: O(1)
  • Total time complexity: O(n * capacity)

Space complexity can often be optimized by:

  • Using rolling arrays (keeping only the last few rows/columns)
  • Using a single array when only the previous state matters

9. Practice Problems by Difficulty

Beginner

  1. Fibonacci Number
  2. Climbing Stairs
  3. Maximum Subarray
  4. House Robber

Intermediate

  1. Longest Increasing Subsequence
  2. Coin Change
  3. Unique Paths
  4. Edit Distance

Advanced

  1. Word Break
  2. Burst Balloons
  3. Regular Expression Matching
  4. Longest Valid Parentheses

10. Summary and Best Practices

When to use Dynamic Programming:

  • Problem exhibits optimal substructure
  • Problem has overlapping subproblems
  • Decision at each step affects future decisions

Tips for Implementation:

  • Start with a recursive solution, then optimize
  • Draw out small examples to visualize the problem
  • Focus on the state transition (recurrence relation)
  • Be careful with base cases and initialization
  • Consider space optimization after getting a working solution

Common Pitfalls:

  • Incorrect state definition
  • Missing base cases
  • Overlooking edge cases
  • Inefficient state representation

Dynamic Programming is a powerful technique that takes practice to master. By understanding these patterns and approaches, you'll be better equipped to recognize and solve DP problems efficiently!