Skip to main content

Dynamic Programming Fundamentals

DP State Transition Visualization

The Mental Model

Dynamic Programming is smart recursion. Instead of solving the same subproblem multiple times, we solve it once and remember the answer. The key insight: if you can express the answer to a problem in terms of answers to smaller problems, DP might work.
Pattern Recognition Signals:
  • “Count the number of ways”
  • “Find minimum/maximum cost”
  • “Can we achieve X?” (feasibility)
  • Problem has overlapping subproblems and optimal substructure
  • Constraints suggest O(n²) or O(n × m) is acceptable

The DP Mindset

When you see a DP problem, think through this framework:
1

Define the State

What information do I need to describe a subproblem?
  • dp[i] = answer for first i elements
  • dp[i][j] = answer for subproblem defined by i and j
2

Find the Transition

How does dp[i] relate to smaller subproblems?
  • dp[i] = f(dp[i-1], dp[i-2], …)
3

Identify Base Cases

What are the simplest subproblems with known answers?
4

Determine the Order

Which subproblems must be solved before others?
5

Extract the Answer

Where in the DP table is the final answer?

Pattern 1: Linear DP

State: dp[i] = answer considering first i elements.

Example: Fibonacci

// Recursive (exponential)
int fib(int n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);
}

// Memoized (top-down DP)
vector<int> memo(n + 1, -1);
int fib(int n) {
    if (n <= 1) return n;
    if (memo[n] != -1) return memo[n];
    return memo[n] = fib(n - 1) + fib(n - 2);
}

// Tabulation (bottom-up DP)
int fib(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
int fib(int n) {
    if (n <= 1) return n;
    int prev2 = 0, prev1 = 1;
    for (int i = 2; i <= n; i++) {
        int curr = prev1 + prev2;
        prev2 = prev1;
        prev1 = curr;
    }
    return prev1;
}

Example: Climbing Stairs

Problem: n stairs, can climb 1 or 2 at a time. Count ways to reach top. State: dp[i] = number of ways to reach stair i Transition: dp[i] = dp[i-1] + dp[i-2] (come from i-1 or i-2)
int climbStairs(int n) {
    if (n <= 2) return n;
    vector<int> dp(n + 1);
    dp[1] = 1;
    dp[2] = 2;
    for (int i = 3; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
}

Pattern 2: Coin Change (Unbounded Knapsack)

Problem: Given coins, find minimum coins to make amount. State: dp[i] = minimum coins to make amount i Transition: dp[i] = min(dp[i - coin] + 1) for each coin
int coinChange(vector<int>& coins, int amount) {
    vector<int> dp(amount + 1, INT_MAX);
    dp[0] = 0;
    
    for (int i = 1; i <= amount; i++) {
        for (int coin : coins) {
            if (coin <= i && dp[i - coin] != INT_MAX) {
                dp[i] = min(dp[i], dp[i - coin] + 1);
            }
        }
    }
    
    return dp[amount] == INT_MAX ? -1 : dp[amount];
}

Counting Ways

Problem: Count ways to make amount.
int countWays(vector<int>& coins, int amount) {
    vector<long long> dp(amount + 1, 0);
    dp[0] = 1;
    
    // Order matters: iterate coins first to avoid counting permutations
    for (int coin : coins) {
        for (int i = coin; i <= amount; i++) {
            dp[i] += dp[i - coin];
        }
    }
    
    return dp[amount];
}
Codeforces Problems:
ProblemRatingLink
Coin Combinations I1500CSES
Coin Combinations II1500CSES

Pattern 3: 0/1 Knapsack

Problem: n items with weight and value. Maximize value in capacity W. Each item used at most once. State: dp[i][w] = max value using first i items with capacity w Transition:
  • Don’t take item i: dp[i][w] = dp[i-1][w]
  • Take item i: dp[i][w] = dp[i-1][w - weight[i]] + value[i]
int knapsack(vector<int>& weight, vector<int>& value, int W) {
    int n = weight.size();
    vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));
    
    for (int i = 1; i <= n; i++) {
        for (int w = 0; w <= W; w++) {
            dp[i][w] = dp[i - 1][w];  // Don't take
            if (weight[i - 1] <= w) {
                dp[i][w] = max(dp[i][w], 
                               dp[i - 1][w - weight[i - 1]] + value[i - 1]);
            }
        }
    }
    
    return dp[n][W];
}

// Space optimized (iterate backwards!)
int knapsack(vector<int>& weight, vector<int>& value, int W) {
    int n = weight.size();
    vector<int> dp(W + 1, 0);
    
    for (int i = 0; i < n; i++) {
        for (int w = W; w >= weight[i]; w--) {  // Backwards!
            dp[w] = max(dp[w], dp[w - weight[i]] + value[i]);
        }
    }
    
    return dp[W];
}
Why Backwards?: If we iterate forwards, we might use item i multiple times (since dp[w - weight[i]] is already updated). Backwards ensures we use the previous row’s values.

Pattern 4: Longest Increasing Subsequence (LIS)

Problem: Find length of longest strictly increasing subsequence. State: dp[i] = length of LIS ending at index i Transition: dp[i] = max(dp[j] + 1) for all j < i where arr[j] < arr[i]
// O(n²) solution
int lis(vector<int>& nums) {
    int n = nums.size();
    vector<int> dp(n, 1);
    int maxLen = 1;
    
    for (int i = 1; i < n; i++) {
        for (int j = 0; j < i; j++) {
            if (nums[j] < nums[i]) {
                dp[i] = max(dp[i], dp[j] + 1);
            }
        }
        maxLen = max(maxLen, dp[i]);
    }
    
    return maxLen;
}

// O(n log n) solution using binary search
int lisOptimized(vector<int>& nums) {
    vector<int> tails;  // tails[i] = smallest ending element for LIS of length i+1
    
    for (int num : nums) {
        auto it = lower_bound(tails.begin(), tails.end(), num);
        if (it == tails.end()) {
            tails.push_back(num);
        } else {
            *it = num;
        }
    }
    
    return tails.size();
}
Key Insight for O(n log n): Maintain the smallest possible tail for each LIS length. Use binary search to find where to place each element. Codeforces Problems:
ProblemRatingLink
Increasing Subsequence1400CSES
Towers1100CF 37D

Pattern 5: Longest Common Subsequence (LCS)

Problem: Find length of longest common subsequence of two strings. State: dp[i][j] = LCS length of s1[0..i-1] and s2[0..j-1] Transition:
  • If s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] + 1
  • Else: dp[i][j] = max(dp[i-1][j], dp[i][j-1])
int lcs(string& s1, string& s2) {
    int m = s1.size(), n = s2.size();
    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 (s1[i - 1] == s2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }
    
    return dp[m][n];
}

Top-Down vs Bottom-Up

AspectTop-Down (Memoization)Bottom-Up (Tabulation)
StyleRecursiveIterative
When computedOn demandAll states
SpaceCan skip unused statesComputes all
DebugStack trace helpsIndex-based
OptimizationHarderEasier (space opt)
Rule of Thumb: Start with top-down (easier to write), convert to bottom-up if needed for optimization.

Common Mistakes

Mistake 1: Wrong Base Case
// WRONG: dp[0] might need to be 1 for counting problems
dp[0] = 0;

// CORRECT for coin change counting
dp[0] = 1;  // One way to make 0: use no coins
Mistake 2: Integer Overflow
// WRONG for counting problems
int dp[n];

// CORRECT
long long dp[n];
// Or use modular arithmetic
dp[i] = (dp[i] + dp[j]) % MOD;
Mistake 3: Wrong Loop Order
  • For 0/1 Knapsack with space optimization: iterate capacity backwards
  • For unbounded knapsack: iterate capacity forwards
Mistake 4: Forgetting States If your DP gives wrong answers, you might be missing state dimensions:
  • Does order matter? → Might need dp[i][j]
  • Does parity matter? → Might need dp[i][0/1]

Practice Problems

Beginner (1100-1300)

ProblemPatternLink
Dice CombinationsLinear DPCSES
Frog 1Linear DPAtCoder DP A
VacationMulti-stateAtCoder DP C

Intermediate (1300-1500)

ProblemPatternLink
Book Shop0/1 KnapsackCSES
BoredomLinear DPCF 455A
Longest IncreasingLISCSES

Advanced (1500-1700)

ProblemPatternLink
Edit DistanceLCS variantCSES
Money SumsSubset sumCSES
Array DescriptionDP with constraintsCF 1800

Key Takeaways

State = Subproblem

The DP state must uniquely identify a subproblem.

Transition = Recursion

The transition is how you’d write the recursive relation.

Base Case = Smallest

Base cases are the smallest subproblems with known answers.

Space Optimize

If dp[i] only depends on dp[i-1], you only need O(1) or O(n) space.

Next Up

Chapter 12: DP on Grids & Advanced

Master 2D DP, interval DP, and bitmask DP for more complex problems.