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:
-
Identify the problem state
- What information do we need to make a decision at each step?
-
Define the recurrence relation
- How does the current state relate to previous states?
-
Identify the base cases
- What are the simplest subproblems that we know the answers to?
-
Decide on a DP approach
- Top-down (memoization) or bottom-up (tabulation)?
-
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
- Fibonacci Number
- Climbing Stairs
- Maximum Subarray
- House Robber
Intermediate
- Longest Increasing Subsequence
- Coin Change
- Unique Paths
- Edit Distance
Advanced
- Word Break
- Burst Balloons
- Regular Expression Matching
- 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!