Skip to content Skip to footer

Dynamic Programming

Introduction

Dynamic Programming (DP) is a powerful algorithmic technique for solving optimization problems by breaking them down into simpler subproblems. It’s particularly effective when a problem has overlapping subproblems and optimal substructure properties.

In this article, we’ll explore Dynamic Programming concepts through practical LeetCode problems, providing clean, well-explained solutions in both C# and Python.

What is Dynamic Programming?

Dynamic Programming is a method for solving complex problems by:

  • Breaking them into simpler subproblems
  • Solving each subproblem only once
  • Storing results to avoid redundant calculations
  • Building up solutions from smaller subproblems

Key DP Concepts:

1. Overlapping Subproblems The same subproblems are solved multiple times. DP stores results to avoid recomputation.

2. Optimal Substructure An optimal solution contains optimal solutions to subproblems.

3. Memoization (Top-Down) Recursive approach with caching. Start from the problem and work down to base cases.

4. Tabulation (Bottom-Up) Iterative approach. Start from base cases and build up to the solution.

When to Use Dynamic Programming:

  • Optimization problems (maximize/minimize)
  • Counting problems (number of ways)
  • Decision problems (can it be done?)
  • Problems with overlapping subproblems
  • Problems with optimal substructure

Problem 1: Longest Increasing Subsequence

Problem Statement

Given an integer array nums, return the length of the longest strictly increasing subsequence.

A subsequence is a sequence that can be derived from an array by deleting some or no elements without changing the order of the remaining elements.

Example

Input: nums = [10,9,2,5,3,7,101,18]
Output: 4
Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4.

Understanding the Problem

For each position i, we need to find the longest increasing subsequence ending at i. We do this by checking all previous positions j < i where nums[j] < nums[i].

Solution Strategy

DP Array Approach:

  1. Initialize dp[i] = 1 for all positions (each element is a subsequence of length 1)
  2. For each position i, look back at all positions j < i
  3. If nums[i] > nums[j], update dp[i] = max(dp[i], dp[j] + 1)
  4. Return the maximum value in the dp array

C# Solution

public class Solution 
{
    public int LengthOfLIS(int[] nums) 
    {
        if (nums.Length == 0)
            return 0;
        
        int size = nums.Length;
        // Initialize dp array: each element is a subsequence of length 1
        int[] dp = Enumerable.Repeat(1, size).ToArray();
        
        for (int i = 1; i < size; i++)
        {
            // Look back at all previous positions
            for (int j = 0; j < i; j++)
            {
                // If current element is greater, extend the subsequence
                if (nums[i] > nums[j])
                {
                    dp[i] = Math.Max(dp[i], dp[j] + 1);
                }
            }
        }
        
        return dp.Max();
    }
}

Python 3 Solution

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums:
            return 0
        
        size = len(nums)
        dp = [1] * size
        
        for i in range(1, size):
            for j in range(i):
                if nums[i] > nums[j]:
                    dp[i] = max(dp[i], 1 + dp[j])
        
        return max(dp)

Complexity Analysis

  • Time Complexity: O(n²) where n is the length of nums
  • Space Complexity: O(n) for the dp array

Problem 2: Maximum Subarray

Problem Statement

Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum.

A subarray is a contiguous part of an array.

Example

Input: nums = [-2,1,-3,4,-1,2,1,-5,4]
Output: 6
Explanation: [4,-1,2,1] has the largest sum = 6.

Understanding the Problem

This is Kadane’s Algorithm. At each position, we decide: should we extend the previous subarray or start a new one? We choose whichever gives a larger sum.

Solution Strategy

Kadane’s Algorithm:

  1. Track currentSum: maximum sum ending at current position
  2. Track maxSum: overall maximum sum seen so far
  3. For each element: currentSum = max(nums[i], currentSum + nums[i])
  4. Update maxSum if currentSum is larger

C# Solution

public class Solution 
{
    public int MaxSubArray(int[] nums) 
    {
        int maxSum = nums[0];
        int currentSum = nums[0];
        
        for (int i = 1; i < nums.Length; i++)
        {
            // Either extend previous subarray or start new one
            currentSum = Math.Max(nums[i], currentSum + nums[i]);
            
            // Update maximum sum seen so far
            maxSum = Math.Max(maxSum, currentSum);
        }
        
        return maxSum;
    }
}

Python 3 Solution

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        currentSum = maxSum = nums[0]
        
        for num in nums[1:]:
            currentSum = max(num, currentSum + num)
            maxSum = max(maxSum, currentSum)
        
        return maxSum

Complexity Analysis

  • Time Complexity: O(n) where n is the length of nums
  • Space Complexity: O(1) – only using constant extra space

Problem 3: Unique Paths

Problem Statement

There is a robot on an m x n grid. The robot is initially located at the top-left corner (grid[0][0]). The robot tries to move to the bottom-right corner (grid[m – 1][n – 1]). The robot can only move either down or right at any point in time.

Given the two integers m and n, return the number of possible unique paths that the robot can take to reach the bottom-right corner.

Understanding the Problem

At each cell, the robot can only come from the cell above (if moving down) or the cell to the left (if moving right). The number of paths to a cell is the sum of paths from top and left cells.

Solution Strategy

DP with Space Optimization:

  1. Initialize a 1D array of size n with all 1s (first row)
  2. For each subsequent row, update: dp[j] = dp[j-1] + dp[j]
  3. dp[j-1] represents paths from left, dp[j] represents paths from top
  4. Return the last element

C# Solution

public class Solution 
{
    public int UniquePaths(int m, int n) 
    {
        // Initialize first row: all cells have 1 path
        int[] dp = Enumerable.Repeat(1, n).ToArray();
        
        // Process each row
        for (int i = 1; i < m; i++)
        {
            // Process each column
            for (int j = 1; j < n; j++)
            {
                // Paths from left + paths from top
                dp[j] = dp[j - 1] + dp[j];
            }
        }
        
        return dp[n - 1];
    }
}

Python 3 Solution

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        dp = [1] * n
        
        for i in range(1, m):
            for j in range(1, n):
                dp[j] = dp[j - 1] + dp[j]
        
        return dp[-1]

Complexity Analysis

  • Time Complexity: O(m × n)
  • Space Complexity: O(n) – optimized from O(m × n)

Problem 4: Unique Paths II

Problem Statement

A robot is located at the top-left corner of a m x n grid. The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid.

Now consider if some obstacles are added to the grids. How many unique paths would there be?

An obstacle and space is marked as 1 and 0 respectively in the grid.

Understanding the Problem

Similar to Unique Paths, but obstacles block certain cells. If a cell has an obstacle, no paths can go through it.

Solution Strategy

DP with Obstacle Handling:

  1. Initialize dp[0] = 1 (starting position)
  2. For each row, process each column
  3. If cell has obstacle, set dp[i] = 0
  4. Otherwise, dp[i] += dp[i-1] (paths from left)
  5. Return the last element

C# Solution

public class Solution 
{
    public int UniquePathsWithObstacles(int[][] obstacleGrid) 
    {
        int width = obstacleGrid[0].Length;
        int[] dp = new int[width];
        
        // Starting position
        dp[0] = 1;
        
        foreach (int[] row in obstacleGrid)
        {
            for (int i = 0; i < width; i++)
            {
                // If obstacle, no paths through this cell
                if (row[i] == 1)
                {
                    dp[i] = 0;
                }
                // Otherwise, add paths from left
                else if (i > 0)
                {
                    dp[i] += dp[i - 1];
                }
            }
        }
        
        return dp[width - 1];
    }
}

Python 3 Solution

class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        width = len(obstacleGrid[0])
        dp = [0] * width
        dp[0] = 1
        
        for row in obstacleGrid:
            for i in range(width):
                if row[i] == 1:
                    dp[i] = 0
                elif i > 0:
                    dp[i] += dp[i - 1]
        
        return dp[-1]

Complexity Analysis

  • Time Complexity: O(m × n)
  • Space Complexity: O(n)

Problem 5: House Robber

Problem Statement

You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed. The only constraint stopping you from robbing each of them is that adjacent houses have security systems connected and it will automatically contact the police if two adjacent houses were broken into on the same night.

Given an integer array nums representing the amount of money of each house, return the maximum amount of money you can rob tonight without alerting the police.

Example

Input: nums = [1,2,3,1]
Output: 4
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3).
Total amount you can rob = 1 + 3 = 4.

Understanding the Problem

At each house, we decide: rob it or skip it. If we rob it, we can’t rob the previous house. We want to maximize the total money.

Solution Strategy

State Variables:

  • previous: Maximum money from houses up to i-2
  • current: Maximum money from houses up to i-1

For each house: current = max(previous + nums[i], current)

C# Solution

public class Solution 
{
    public int Rob(int[] nums) 
    {
        // previous: maximum gain using houses up to i-2
        // current: maximum gain using houses up to i-1
        int previous = 0, current = 0;
        
        foreach (int num in nums)
        {
            // Either rob current house + previous max, or skip current house
            int temp = Math.Max(previous + num, current);
            
            // Update for next iteration
            previous = current;
            current = temp;
        }
        
        return current;
    }
}

Python 3 Solution

class Solution:
    def rob(self, nums: List[int]) -> int:
        previous, current = 0, 0
        
        for num in nums:
            temp = max(previous + num, current)
            previous = current
            current = temp
        
        return current

Complexity Analysis

  • Time Complexity: O(n) where n is the number of houses
  • Space Complexity: O(1) – only using two variables

Problem 6: House Robber II

Problem Statement

You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed. All houses at this place are arranged in a circle. That means the first house is the neighbor of the last one. Meanwhile, adjacent houses have a security system connected, and it will automatically contact the police if two adjacent houses were broken into on the same night.

Given an integer array nums representing the amount of money of each house, return the maximum amount of money you can rob tonight without alerting the police.

Example

Input: nums = [2,3,2]
Output: 3
Explanation: You cannot rob house 1 (money = 2) and then rob house 3 (money = 2), because they are adjacent houses.

Understanding the Problem

Since houses are in a circle, the first and last houses are adjacent. We need to consider two cases:

  1. Rob houses from index 0 to n-2 (exclude last)
  2. Rob houses from index 1 to n-1 (exclude first)

Take the maximum of both cases.

Solution Strategy

Two-Pass Approach:

  1. First pass: Rob houses 0 to n-2 (exclude last house)
  2. Second pass: Rob houses 1 to n-1 (exclude first house)
  3. Return maximum of both cases

C# Solution

public class Solution 
{
    public int Rob(int[] nums) 
    {
        if (nums.Length == 0) 
            return 0;
        
        if (nums.Length == 1) 
            return nums[0];
        
        // Case 1: Rob houses 0 to n-2 (exclude last)
        int previousNotRobbed = 0, currentNotRobbed = 0;
        
        // Case 2: Rob houses 1 to n-1 (exclude first)
        int previousRobbed = 0, currentRobbed = 0;
        
        for (int i = 0; i < nums.Length - 1; i++)
        {
            // Case 1: Exclude last house
            int tempNotRobbed = previousNotRobbed;
            previousNotRobbed = currentNotRobbed;
            currentNotRobbed = Math.Max(tempNotRobbed + nums[i], previousNotRobbed);
            
            // Case 2: Exclude first house (shift by 1)
            int tempRobbed = previousRobbed;
            previousRobbed = currentRobbed;
            currentRobbed = Math.Max(tempRobbed + nums[i + 1], previousRobbed);
        }
        
        return Math.Max(currentRobbed, currentNotRobbed);
    }
}

Python 3 Solution

class Solution:
    def rob(self, nums: List[int]) -> int:
        if len(nums) == 0:
            return 0
        
        if len(nums) == 1:
            return nums[0]
        
        previousRobbed, currentRobbed = 0, 0
        previousNotRobbed, currentNotRobbed = 0, 0
        
        for i in range(len(nums) - 1):
            tempNotRobbed = previousNotRobbed
            previousNotRobbed = currentNotRobbed
            currentNotRobbed = max(tempNotRobbed + nums[i], previousNotRobbed)
            
            tempRobbed = previousRobbed
            previousRobbed = currentRobbed
            currentRobbed = max(tempRobbed + nums[i + 1], previousRobbed)
        
        return max(currentRobbed, currentNotRobbed)

Complexity Analysis

  • Time Complexity: O(n) where n is the number of houses
  • Space Complexity: O(1)

Problem 7: Best Time to Buy and Sell Stock

Problem Statement

You are given an array prices where prices[i] is the price of a given stock on the ith day.

You want to maximize your profit by choosing a single day to buy one stock and choosing a different day in the future to sell that stock.

Return the maximum profit you can achieve from this transaction. If you cannot achieve any profit, return 0.

Example

Input: prices = [7,1,5,3,6,4]
Output: 5
Explanation: Buy on day 2 (price = 1) and sell on day 5 (price = 6), profit = 6-1 = 5.

Understanding the Problem

We need to find the maximum difference between a later price and an earlier price. Track the minimum price seen so far and the maximum profit.

Solution Strategy

Single Pass:

  1. Track minPrice: minimum price seen so far
  2. Track maxProfit: maximum profit achievable
  3. For each price: update min and calculate profit

C# Solution

public class Solution 
{
    public int MaxProfit(int[] prices) 
    {
        int maxProfit = 0;
        int minPrice = int.MaxValue;
        
        foreach (int price in prices)
        {
            // Update minimum price seen so far
            minPrice = Math.Min(price, minPrice);
            
            // Calculate profit if selling today
            maxProfit = Math.Max(price - minPrice, maxProfit);
        }
        
        return maxProfit;
    }
}

Python 3 Solution

import sys

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        maxProfit = 0
        minPrice = sys.maxsize
        
        for price in prices:
            minPrice = min(price, minPrice)
            maxProfit = max(price - minPrice, maxProfit)
        
        return maxProfit

Complexity Analysis

  • Time Complexity: O(n) where n is the length of prices
  • Space Complexity: O(1)

Problem 8: Best Time to Buy and Sell Stock II

Problem Statement

You are given an integer array prices where prices[i] is the price of a given stock on the ith day.

On each day, you may decide to buy and/or sell the stock. You can only hold at most one share of the stock at any time. However, you can buy it then immediately sell it on the same day.

Find and return the maximum profit you can achieve.

Example

Input: prices = [7,1,5,3,6,4]
Output: 7
Explanation: Buy on day 2 (price = 1) and sell on day 3 (price = 5), profit = 5-1 = 4.
Then buy on day 4 (price = 3) and sell on day 5 (price = 6), profit = 6-3 = 3.
Total profit is 4 + 3 = 7.

Understanding the Problem

We can make multiple transactions. The key insight: we should buy before every price increase and sell before every price decrease. Essentially, we capture every upward price movement.

Solution Strategy

State Machine Approach:

  • currentHold: Maximum profit while holding a stock
  • currentNotHold: Maximum profit while not holding a stock

At each day, we can either:

  • Keep holding or buy (if not holding)
  • Keep not holding or sell (if holding)

C# Solution

public class Solution 
{
    public int MaxProfit(int[] prices) 
    {
        // Cannot sell on first day, so initialize with negative infinity
        int currentHold = int.MinValue;
        int currentNotHold = 0;
        
        foreach (int price in prices)
        {
            int previousHold = currentHold;
            int previousNotHold = currentNotHold;
            
            // Either keep holding, or buy today
            currentHold = Math.Max(previousHold, previousNotHold - price);
            
            // Either keep not holding, or sell today
            currentNotHold = Math.Max(previousNotHold, previousHold + price);
        }
        
        return prices.Length > 0 ? currentNotHold : 0;
    }
}

Python 3 Solution

import sys

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        currentHold, currentNotHold = -sys.maxsize, 0
        
        for price in prices:
            previousHold, previousNotHold = currentHold, currentNotHold
            
            currentHold = max(previousHold, previousNotHold - price)
            currentNotHold = max(previousNotHold, previousHold + price)
        
        return currentNotHold if prices else 0

Complexity Analysis

  • Time Complexity: O(n) where n is the length of prices
  • Space Complexity: O(1)

Problem 9: Word Break

Problem Statement

Given a string s and a dictionary of strings wordDict, return true if s can be segmented into a space-separated sequence of one or more dictionary words.

Note that the same word in the dictionary may be reused multiple times in the segmentation.

Example

Input: s = "leetcode", wordDict = ["leet","code"]
Output: true
Explanation: Return true because "leetcode" can be segmented as "leet code".

Understanding the Problem

We need to check if the string can be broken into dictionary words. Use DP: dp[i] represents whether substring s[0..i] can be segmented.

Solution Strategy

DP Array:

  1. dp[0] = true (empty string can be segmented)
  2. For each position i, check all positions j < i
  3. If dp[j] is true and s[j..i] is in dictionary, set dp[i] = true
  4. Return dp[n]

C# Solution

public class Solution 
{
    public bool WordBreak(string s, IList<string> wordDict) 
    {
        int size = s.Length;
        bool[] dp = new bool[size + 1];
        dp[0] = true; // Empty string can be segmented
        
        for (int i = 0; i <= size; i++)
        {
            if (dp[i])
            {
                // Check all substrings starting from position i
                for (int j = i + 1; j <= size; j++)
                {
                    string substring = s.Substring(i, j - i);
                    if (wordDict.Contains(substring))
                    {
                        dp[j] = true;
                    }
                }
            }
        }
        
        return dp[size];
    }
}

Python 3 Solution

class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        size = len(s)
        dp = [False] * (size + 1)
        dp[0] = True
        
        for i in range(size + 1):
            if dp[i]:
                for j in range(i + 1, size + 1):
                    if s[i:j] in wordDict:
                        dp[j] = True
        
        return dp[-1]

Complexity Analysis

  • Time Complexity: O(n² × m) where n is string length and m is average word length
  • Space Complexity: O(n) for the dp array

Problem 10: Coin Change

Problem Statement

You are given an integer array coins representing coins of different denominations and an integer amount representing a total amount of money.

Return the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1.

You may assume that you have an infinite number of each kind of coin.

Example

Input: coins = [1,2,5], amount = 11
Output: 3
Explanation: 11 = 5 + 5 + 1

Understanding the Problem

For each amount from 1 to amount, we find the minimum coins needed. We try each coin and take the minimum.

Solution Strategy

Bottom-Up DP:

  1. Initialize dp[i] = infinity for all amounts (impossible initially)
  2. dp[0] = 0 (0 coins needed for amount 0)
  3. For each amount i, try each coin: dp[i] = min(dp[i], dp[i-coin] + 1)
  4. Return dp[amount] or -1 if still infinity

C# Solution

public class Solution 
{
    public int CoinChange(int[] coins, int amount) 
    {
        int size = amount + 1;
        // Initialize with infinity (impossible)
        float[] dp = Enumerable.Repeat(float.PositiveInfinity, size).ToArray();
        dp[0] = 0; // 0 coins needed for amount 0
        
        for (int i = 1; i < amount + 1; i++)
        {
            foreach (int coin in coins)
            {
                if (i - coin >= 0)
                {
                    // Try using this coin
                    dp[i] = Math.Min(dp[i], dp[i - coin] + 1);
                }
            }
        }
        
        if (dp[amount] == float.PositiveInfinity)
            return -1;
        
        return (int)dp[amount];
    }
}

Python 3 Solution

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        size = amount + 1
        dp = [float('inf')] * size
        dp[0] = 0
        
        for i in range(1, size):
            for coin in coins:
                if i - coin >= 0:
                    dp[i] = min(dp[i], dp[i - coin] + 1)
        
        if dp[-1] == float('inf'):
            return -1
        
        return dp[-1]

Complexity Analysis

  • Time Complexity: O(amount × coins.length)
  • Space Complexity: O(amount)

Key Takeaways

Dynamic Programming Patterns:

1. 1D DP Array

  • Longest Increasing Subsequence
  • House Robber
  • Coin Change

2. 2D DP Array (or 1D optimized)

  • Unique Paths
  • Grid-based problems

3. State Machine

  • Best Time to Buy and Sell Stock II
  • Problems with multiple states

4. String DP

  • Word Break
  • Palindrome problems

Common DP Techniques:

Memoization (Top-Down):

  • Recursive with caching
  • Natural problem structure
  • May have stack overflow for deep recursion

Tabulation (Bottom-Up):

  • Iterative approach
  • Better space optimization
  • No recursion overhead

Space Optimization:

  • Reduce 2D to 1D when possible
  • Use rolling arrays
  • Track only necessary previous states

Problem-Solving Steps:

  1. Identify DP pattern: Does it have overlapping subproblems?
  2. Define state: What does dp[i] represent?
  3. Find recurrence relation: How to compute dp[i] from previous states?
  4. Base cases: What are the initial values?
  5. Optimize space: Can we reduce space complexity?

When to Use DP:

  • Optimization problems (max/min)
  • Counting problems
  • Decision problems
  • Problems asking “how many ways”
  • Problems with overlapping subproblems
  • Problems with optimal substructure

Conclusion

Dynamic Programming is a powerful technique for solving optimization and counting problems. The key is recognizing when a problem has overlapping subproblems and optimal substructure, then building a solution from smaller subproblems.

Key patterns to remember:

  • 1D DP: Linear problems, sequences
  • 2D DP: Grid problems, two sequences
  • State Machine: Problems with multiple states
  • Bottom-Up: Build from base cases
  • Top-Down: Break down from problem

Practice identifying DP patterns and building recurrence relations. Start with simpler problems and gradually work up to more complex ones. The more you practice, the better you’ll become at recognizing when and how to apply Dynamic Programming.

Happy coding!

Leave a Comment