Dynamic Programming

1D Dynamic Programming

Top Down

def fib(self, n: int) -> int:
	memo = {0: 0, 1: 1}

	def bruteForce(n):
		if n <= 1:
			return n
		if n in memo:
			return memo[n]
		memo[n] = bruteForce(n-1) + bruteForce(n-2)
		return memo[n]

	return bruteForce(n)

Bottom Up

def fib(self, n: int) -> int:
	if n <= 1:
		return n
	
	a, b = 0, 1

	for _ in range(2, n):
		a, b = b, a + b
	return a + b

2D Dynamic Programming

Palindrome

Odd length: where there is a single character at the center (e.g., “racecar”, center is ‘e’).
Even length: where there are two center characters (e.g., “abba”, centers are ‘bb’).

Since a palindrome can potentially start from any index in the string, you need to check around each character (odd-length palindromes) and between each pair of characters (even-length palindromes). The for loop ensures that every possible center in the string is examined for both types of palindromes.

Without the for loop, the solution would not explore all possible centers and would miss some palindromic substrings.

def longestPalindrome(self, s: str) -> str:
	longest = 0  # Length of the longest palindrome found so far
	leftMost = 0  # Left index of the longest palindrome
	rightMost = 0  # Right index of the longest palindrome

	for i in range(len(s)):
		# Check for even-length palindromes (e.g., "baab")
		l, r = i, i + 1  # Start with two middle points
		while l >= 0 and r < len(s) and s[l] == s[r]:  # Expand outwards while characters match
			if r - l + 1 > longest:  # Update if the current palindrome is longer
				longest = (r - l + 1)
				leftMost = l
				rightMost = r
			l -= 1  # Move left pointer outwards
			r += 1  # Move right pointer outwards

		# Check for odd-length palindromes (e.g., "babad")
		l, r = i, i  # Start with one middle point
		while l >= 0 and r < len(s) and s[l] == s[r]:  # Expand outwards while characters match
			if r - l + 1 > longest:  # Update if the current palindrome is longer
				longest = (r - l + 1)
				leftMost = l
				rightMost = r
			l -= 1  # Move left pointer outwards
			r += 1  # Move right pointer outwards

	# Return the substring from the stored left and right indices
	return s[leftMost:rightMost + 1]

0/1 Knapsack

Memoization

def dfs(profit, weight, capacity):
	return dfsHelper(0, profit, weight, capacity)

@cache
def dfsHelper(i, profit, weight, capacity):
	if i == len(profit):
		return 0

	# Skip item i - Incrementing the index but not reducing capacity
	maxProfit = dfsHelper(i + 1, profit, weight, capacity)

	# Include item i - Incrementing the index but reducing the capactiy
	newCap = capacity - weight[i]
	if newCap >= 0:
		newProfit = profit[i] + dfsHelper(i + 1, profit, weight, newCap)
		# Compute the max
		maxProfit = max(maxProfit, newProfit)
	
	return maxProfit

True DP Solution

def knapSack(self, maxWeight, weights, values):
    numItems, capacity = len(values), maxWeight
    # dp[i][j] will store the maximum value for the first i items with a capacity of j
    dp = [[0] * (capacity + 1) for _ in range(numItems)]

    # Initialize the first column (0 capacity means 0 value)
    for i in range(numItems):
        dp[i][0] = 0

    # Initialize the first row (only one item can be chosen)
    for currCapacity in range(capacity + 1):
        dp[0][currCapacity] = values[0] if currCapacity >= weights[0] else 0

    # Fill the dp table by considering each item and capacity
    for item in range(1, numItems):
        for currCapacity in range(1, capacity + 1):
            # Case 1: Don't include the current item
            maxProfitWithoutCurrent = dp[item - 1][currCapacity]

            # Case 2: Include the current item (only if it fits)
            if currCapacity - weights[item] >= 0:
                maxProfitWithCurrent = values[item] + dp[item - 1][currCapacity - weights[item]]
                # Take the maximum of including or not including the current item
                dp[item][currCapacity] = max(maxProfitWithoutCurrent, maxProfitWithCurrent)
            else:
                # If current item can't be included, carry forward the previous profit
                dp[item][currCapacity] = maxProfitWithoutCurrent

    # The answer will be the maximum value for all items with the full capacity
    return dp[numItems - 1][capacity]

Unbounded Knapsack

Memoization

def dfs(profit, weight, capacity):
	return dfsHelper(0, profit, weight, capacity)

@cache
def dfsHelper(i, profit, weight, capacity):
	if i == len(profit):
		return 0

	# Skip item i - Incrementing the index but not reducing capacity
	maxProfit = dfsHelper(i + 1, profit, weight, capacity)

	# NOT INCREMENTING i is the only diff between bounded and unbounded
	# Include item i - NOT incrementing the index but reducing the capactiy
	newCap = capacity - weight[i]
	if newCap >= 0:
		newProfit = profit[i] + dfsHelper(i, profit, weight, newCap)
		# Compute the max
		maxProfit = max(maxProfit, newProfit)
	
	return maxProfit

True DP

def knapSack(self, maxWeight, weights, values):
    numItems, capacity = len(values), maxWeight
    # dp[i][j] will store the maximum value for the first i items with a capacity of j
    dp = [[0] * (capacity + 1) for _ in range(numItems)]

    # Initialize the first column (0 capacity means 0 value)
    for i in range(numItems):
        dp[i][0] = 0

    # Initialize the first row (only one item can be chosen)
    for currCapacity in range(capacity + 1):
        dp[0][currCapacity] = values[0] if currCapacity >= weights[0] else 0

    # Fill the dp table by considering each item and capacity
    for item in range(1, numItems):
        for currCapacity in range(1, capacity + 1):
            # Case 1: Don't include the current item
            maxProfitWithoutCurrent = dp[item - 1][currCapacity]

            # Case 2: Include the current item (only if it fits)
            if currCapacity - weights[item] >= 0:
	            ## DID NOT LOOK FOR PREVIOUS ITEM's value
                maxProfitWithCurrent = values[item] + dp[item][currCapacity - weights[item]]
                # Take the maximum of including or not including the current item
                dp[item][currCapacity] = max(maxProfitWithoutCurrent, maxProfitWithCurrent)
            else:
                # If current item can't be included, carry forward the previous profit
                dp[item][currCapacity] = maxProfitWithoutCurrent

    # The answer will be the maximum value for all items with the full capacity
    return dp[numItems - 1][capacity]

Most Optimized with Memory

def optimizedDp(profit, weight, capacity):
	N, M = len(profit), capacity
	dp = [0] * (M + 1) # only creating one row

	for i in range(N):
		curRow = [0] * (M + 1) # creating a temp row
		for c in range(1, M + 1):
			skip = dp[c]
			include = 0
			if c - weight[i] >= 0:
				include = profit[i] + curRow[c - weight[i]]
			curRow[c] = max(include, skip)
		dp = curRow
	return dp[M]

Longest Common Subsequence