
Dynamic Programming (DP) is a powerful algorithmic technique used to solve a wide range of optimization problems. It's a cornerstone of technical interviews, and LeetCode provides an extensive collection of DP problems to help you master this crucial skill. In this comprehensive guide, we'll explore LeetCode's top DP problems, dissecting their statements, approaches, solution strategies (Top-Down and Bottom-Up), and identifying similar problems across various DP patterns.
Problem Statement: You are given an array cost where cost[i] represents the cost of climbing the i-th stair. You can start climbing from either the 0-th or 1-st stair. Each time you can either climb one or two steps. Return the minimum cost to reach the top of the floor.
Approach:
This problem exhibits a classic Dynamic Programming pattern where the minimum cost to reach a stair is the minimum of the costs of reaching the previous two stairs plus the cost of the current stair.
minCostClimbingStairsTopDown, which takes the current index n and the array cost as inputs.n-th stair.def minCostClimbingStairsTopDown(cost, n, memo):
if n <= 1:
return cost[n]
if memo[n] != -1:
return memo[n]
memo[n] = min(minCostClimbingStairsTopDown(cost, n-1, memo), minCostClimbingStairsTopDown(cost, n-2, memo)) + cost[n]
return memo[n]
def minCostClimbingStairs(cost):
n = len(cost)
memo = [-1] * n
return min(minCostClimbingStairsTopDown(cost, n-1, memo), minCostClimbingStairsTopDown(cost, n-2, memo))dp of size n+1 to store the minimum cost to reach each stair.def minCostClimbingStairsBottomUp(cost):
n = len(cost)
dp = [0] * (n+1)
dp[0], dp[1] = cost[0], cost[1]
for i in range(2, n):
dp[i] = min(dp[i-1], dp[i-2]) + cost[i]
return min(dp[n-1], dp[n-2])cost = [10, 15, 20]
print(minCostClimbingStairsBottomUp(cost)) # Output: 15Similar Problems:
n stairs, you can climb 1 or 2 steps at a time. Determine the number of distinct ways to reach the top.def climbStairsTopDown(n, memo):
if n <= 1:
return 1
if memo[n] != -1:
return memo[n]
memo[n] = climbStairsTopDown(n-1, memo) + climbStairsTopDown(n-2, memo)
return memo[n]
def climbStairs(n):
memo = [-1] * (n + 1)
return climbStairsTopDown(n, memo)def climbStairsBottomUp(n):
if n <= 1:
return 1
dp = [0] * (n + 1)
dp[0], dp[1] = 1, 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
n and a list of coin denominations, determine the minimum number of coins needed to make up that amount.def coinChangeTopDown(coins, amount, memo):
if amount < 0:
return -1
if amount == 0:
return 0
if memo[amount] != float('inf'):
return memo[amount]
for coin in coins:
subproblem = coinChangeTopDown(coins, amount - coin, memo)
if subproblem >= 0:
memo[amount] = min(memo[amount], 1 + subproblem)
return memo[amount] if memo[amount] != float('inf') else -1
def coinChange(coins, amount):
memo = [float('inf')] * (amount + 1)
return coinChangeTopDown(coins, amount, memo)Analysis:
Problem Statement: Given a collection of intervals, merge all overlapping intervals.
Approach:
The key to solving merging interval problems lies in identifying the optimal solution for each interval by considering the best from the left and right sides, then merging them as necessary.
mergeIntervalsTopDown, which takes the collection of intervals intervals, the start index start, the end index end, and a memoization table memo.def mergeIntervalsTopDown(intervals, start, end, memo):
if start == end:
return intervals[start][0], intervals[start][1]
if memo[start][end]:
return memo[start][end]
for i in range(start, end):
left_start, left_end = mergeIntervalsTopDown(intervals, start, i, memo)
right_start, right_end = mergeIntervalsTopDown(intervals, i+1, end, memo)
if left_end >= right_start:
memo[start][end] = left_start, right_end
return memo[start][end]
memo[start][end] = intervals[start][0], intervals[end][1]
return memo[start][end]
def merge(intervals):
n = len(intervals)
if n == 0:
return []
memo = [[None]*n for _ in range(n)]
return mergeIntervalsTopDown(intervals, 0, n-1, memo)merged to store the merged intervals.def mergeIntervalsBottomUp(intervals):
if not intervals:
return []
intervals.sort(key=lambda x: x[0])
merged = []
for interval in intervals:
if not merged or interval[0] > merged[-1][1]:
merged.append(interval)
else:
merged[-1][1] = max(merged[-1][1], interval[1])
return mergedintervals = [[1,3],[2,6],[8,10],[15,18]]
print(merge(intervals)) # Output: [[1,6],[8,10],[15,18]]Similar Problems:
def insertInterval(intervals, newInterval):
merged = []
i = 0
while i < len(intervals) and intervals[i][1] < newInterval[0]:
merged.append(intervals[i])
i += 1
while i < len(intervals) and intervals[i][0] <= newInterval[1]:
newInterval = [min(newInterval[0], intervals[i][0]), max(newInterval[1], intervals[i][1])]
i += 1
merged.append(newInterval)
merged.extend(intervals[i:])
return mergeddef insertInterval(intervals, newInterval):
merged = []
i = 0
while i < len(intervals) and intervals[i][1] < newInterval[0]:
merged.append(intervals[i])
i += 1
while i < len(intervals) and intervals[i][0] <= newInterval[1]:
newInterval = [min(newInterval[0], intervals[i][0]), max(newInterval[1], intervals[i][1])]
i += 1
merged.append(newInterval)
merged.extend(intervals[i:])
return mergedimport heapq
def minMeetingRooms(intervals):
if not intervals:
return 0
intervals.sort(key=lambda x: x[0])
rooms = []
heapq.heappush(rooms, intervals[0][1])
for interval in intervals[1:]:
if interval[0] >= rooms[0]:
heapq.heappop(rooms)
heapq.heappush(rooms, interval[1])
return len(rooms)import heapq
def minMeetingRooms(intervals):
if not intervals:
return 0
intervals.sort(key=lambda x: x[0])
rooms = []
heapq.heappush(rooms, intervals[0][1])
for interval in intervals[1:]:
if interval[0] >= rooms[0]:
heapq.heappop(rooms)
heapq.heappush(rooms, interval[1])
return len(rooms)Analysis:
The Bottom-Up approach has a time complexity of O(n log n) and a space complexity of O(n) due to the sorting of intervals.
Problem Statement: Dynamic Programming on strings involves solving problems that require processing or comparing strings optimally.
Approach:
Most problems in this category involve building a 2D DP table to store intermediate results, where the value at dp[i][j] represents the optimal solution considering substrings s1[:i] and s2[:j].
Problem Statement: Given two strings text1 and text2, return the length of their longest common subsequence.
Approach:
We build a 2D DP table dp where dp[i][j] represents the length of the longest common subsequence of text1[:i] and text2[:j].
text1[i-1] == text2[j-1], we extend the common subsequence by 1: dp[i][j] = dp[i-1][j-1] + 1.text1 or text2: dp[i][j] = max(dp[i-1][j], dp[i][j-1]).Similar Problems:
s, find the length of the longest palindromic subsequence in s.word1 and word2, find the minimum number of operations required to convert word1 to word2.Code Implementation:
def longestCommonSubsequence(text1: str, text2: str) -> int:
m, n = len(text1), len(text2)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i - 1] == text2[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]text1 = "abcde"
text2 = "ace"
print(longestCommonSubsequence(text1, text2)) # Output: 3 (LCS: "ace")Analysis:
text1 and text2, respectively.Problem Statement: Decision-making problems involve determining whether to include or exclude the current state to optimize a given objective.
Approach:
These problems usually involve constructing a DP table where dp[i][j] represents the maximum value or minimum cost achievable up to the i-th state by considering j options.
Problem Statement: You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, and 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. Determine the maximum amount of money you can rob tonight without alerting the police.
Approach:
We use a DP approach to calculate the maximum amount of money that can be robbed up to the i-th house while considering two options: either robbing the current house or skipping it.
i-th house, we add its value to the maximum loot from the previous non-adjacent house.i-th house, we take the maximum loot up to the i-1-th house.Similar Problems:
Problem Statement: Similar to Problem #198, but the houses are arranged in a circular manner.
Approach:
To solve this problem, we can use dynamic programming to find the maximum loot that can be obtained while considering two cases:
We then return the maximum loot obtained from these two cases.
def rob(nums):
if len(nums) == 1:
return nums[0]
return max(robHelper(nums[:-1]), robHelper(nums[1:]))
def robHelper(nums):
if not nums:
return 0
if len(nums) == 1:
return nums[0]
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, len(nums)):
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])
return dp[-1]def rob(nums):
if len(nums) == 1:
return nums[0]
return max(robHelper(nums[:-1]), robHelper(nums[1:]))
def robHelper(nums):
if not nums:
return 0
if len(nums) == 1:
return nums[0]
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, len(nums)):
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])
return dp[-1]Similar Problems:
Problem Statement: Given an array of stock prices, find the maximum profit that can be obtained by buying and selling at most once.
Approach:
To solve this problem, we can use dynamic programming to keep track of the maximum profit that can be obtained by buying and selling the stock at each day.
def maxProfit(prices):
def helper(index, bought):
if index >= len(prices):
return 0
if (index, bought) in memo:
return memo[(index, bought)]
if bought:
# If currently holding a stock, we can either sell it or skip this day
sell_today = prices[index] + helper(index + 1, False)
skip_today = helper(index + 1, True)
memo[(index, bought)] = max(sell_today, skip_today)
else:
# If not holding a stock, we can either buy a stock or skip this day
buy_today = -prices[index] + helper(index + 1, True)
skip_today = helper(index + 1, False)
memo[(index, bought)] = max(buy_today, skip_today)
return memo[(index, bought)]
if not prices:
return 0
memo = {}
return helper(0, False)prices = [7, 1, 5, 3, 6, 4]
print(maxProfit(prices)) # Output should be 5
def maxProfit(prices):
if not prices:
return 0
max_profit = 0
min_price = prices[0]
for price in prices[1:]:
max_profit = max(max_profit, price - min_price)
min_price = min(min_price, price)
return max_profitSimilar Problems:
Code Implementation:
def rob(nums: List[int]) -> int:
n = len(nums)
if n == 0:
return 0
if n == 1:
return nums[0]
dp = [0] * n
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, n):
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])
return dp[-1]nums = [2,7,9,3,1]
print(rob(nums)) # Output: 12 (Rob houses 1, 3, and 5 for a total of 12)Analysis: