Understanding Dynamic Programming (DP)
Introduction
Dynamic Programming (DP) is one of the most powerful techniques in algorithm design. It is used to solve complex problems by breaking them into smaller overlapping subproblems and storing their results to avoid repeating calculations.
DP provides huge performance improvements over naive recursive solutions.
Dynamic Programming is heavily used in:
Optimization problems
Pathfinding
String matching
Game theory
Finance and resource allocation
When Should You Use Dynamic Programming?
A problem can be solved using DP when it has two key properties:
1. Overlapping Subproblems
The same subproblems repeat many times.
Example: Fibonacci sequence
F(5) = F(4) + F(3)
F(4) = F(3) + F(2)
Here, F(3) is calculated multiple times.
2. Optimal Substructure
The optimal solution is built from optimal solutions of smaller sub-problems.
Example: Shortest path in a grid.
Two Approaches to Dynamic Programming
Dynamic Programming can be implemented in two ways:
1. Top-Down (Memoization)
Use recursion
Store results in a memo table
2. Bottom-Up (Tabulation)
Build solution iteratively
Start from base cases and fill a DP table
Both methods improve performance significantly.
Example 1: Fibonacci Number
Naive Recursive Solution (Slow)
int Fibonacci(int n)
{
if (n <= 1) return n;
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Time complexity: O(2n) ? extremely slow
Memoization (Top-Down DP)
int FibonacciDP(int n, int[] memo)
{
if (n <= 1) return n;
if (memo[n] != -1) return memo[n];
memo[n] = FibonacciDP(n - 1, memo) + FibonacciDP(n - 2, memo);
return memo[n];
}
Time complexity: O(n)
Space complexity: O(n)
Tabulation (Bottom-Up DP)
int FibonacciTab(int n)
{
int[] dp = new int[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];
}
This approach is iterative and avoids recursion.
Example 2: Longest Common Subsequence (LCS)
LCS finds the longest common subsequence between the two strings.
Problem Example
String A = "abcde"
String B = "ace"
LCS = "ace"
DP Approach
Create a 2D DP table.
int LCS(string a, string b)
{
int m = a.Length;
int n = b.Length;
int[,] dp = new int[m + 1, n + 1];
for (int i = 1; i <= m; i++)
{
for (int j = 1; j <= n; j++)
{
if (a[i - 1] == b[j - 1])
dp[i, j] = dp[i - 1, j - 1] + 1;
else
dp[i, j] = Math.Max(dp[i - 1, j], dp[i, j - 1]);
}
}
return dp[m, n];
}
Example 3: 0/1 Knapsack Problem
You have items with weights and values. Choose items such that:
Total weight = capacity
Total value is maximized
DP Approach
int Knapsack(int[] wt, int[] val, int W)
{
int n = wt.Length;
int[,] dp = new int[n + 1, W + 1];
for (int i = 1; i <= n; i++)
{
for (int w = 1; w <= W; w++)
{
if (wt[i - 1] <= w)
{
dp[i, w] = Math.Max(val[i - 1] + dp[i - 1, w - wt[i - 1]], dp[i - 1, w]);
}
else
{
dp[i, w] = dp[i - 1, w];
}
}
}
return dp[n, W];
}
Real-Life Applications of Dynamic Programming
1. Pathfinding Algorithms
Google Maps shortest route
Game movement
2. Bioinformatics
DNA sequence alignment
3. Finance
Stock profit maximization
4. Robotics
Optimal movement planning
5. AI and Machine Learning
Markov decision processes
6. Operating Systems
Resource allocation
When Not to Use Dynamic Programming
DP should not be used when:
Subproblems do not overlap
Greedy algorithms produce optimal results
Memory is limited (DP requires tables)
Summary
Dynamic Programming solves problems efficiently by storing solutions to subproblems and avoiding repeated work.
Key takeaways:
DP = Recursion + Memoization
Overlapping subproblems + Optimal substructure required
Used for Fibonacci, LCS, Knapsack, pathfinding, and optimization