Skip to content

Dynamic Programming

Summary

The key idea to solve a dynamic programming (DP) problem is to break it down into subproblems and write the state transition function. In the mean-time, DP problem might have redundant subproblems, so we can use memoization or a DP table to reduce the redundant computation.

Fibonacci Sequence (Leetcode 509)

Brute Force Solution

The brute force solution is to use recursion to calculate the Fibonacci number. However, this solution has exponential time complexity (\(\gO(2^n)\))

class Solution:
    def fib(self, n: int) -> int:

        if n <= 0:
            return 0
        elif n == 1:
            return 1
        else:
            return self.fib(n-1) + self.fib(n-2)

Memoization Solution

To reduce the redundant computation, we can use memoization to store the intermediate results.

class Solution:
    def fib(self, n: int) -> int:
        self.mem = [False] * (n+1)
        return self.dp(n)


    def dp(self, n):
        if n <= 0:
            return 0
        elif n == 1:
            return 1

        if not self.mem[n]:
            self.mem[n] = self.dp(n-1) + self.dp(n-2)

Bottom-up Solution (Iterative)

We may not need to store all the intermediate results since the current state only depends on the previous two states. Thus, we can use a bottom-up approach to reduce the space complexity to \(\gO(1)\). The state transition function is \(f(n) = f(n-1) + f(n-2)\).

class Solution:
    def fib(self, n: int) -> int:

        if n == 0 or n == 1:
            return n

        fib_n1, fib_n2 = 1, 0

        for i in range(2, n+1):
            fib_n = fib_n1 + fib_n2
            fib_n1, fib_n2 = fib_n, fib_n1

        return fib_n

Coin Change (Leetcode 322)

Brute Force Solution

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        return self.dp(coins, amount)


    def dp(self, coins, amount):

        if amount == 0:
            return 0
        elif amount <= 0:
            return -1

        res = float('inf')
        for coin in coins:
            count = self.dp(coins,amount - coin)

            if count < 0:
                continue

            res = min(res, count + 1)

        return res if res != float('inf') else -1

Memoization Solution

The following is a wrong solution because the memoization table is not updated correctly.

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        self.mem = [-1] * (amount + 1) # Use another value to indicate it is not calculated yet
        return self.dp(coins, amount)


    def dp(self, coins, amount):

        if amount == 0:
            return 0
        elif amount < 0:
            return -1

        if self.mem[amount] > 0:
            return self.mem[amount]

        res = float('inf')
        for coin in coins:
            count = self.dp(coins,amount - coin)

            if count < 0:
                continue

            res = min(res, count + 1)

        if res != float('inf'):
            self.mem[amount] = res # This should be updated to -1 if res == float('inf')

        return self.mem[amount]

The correct memoization solution is as follows:

class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        self.mem = [-2] * (amount + 1)
        return self.dp(coins, amount)


    def dp(self, coins, amount):

        if amount == 0:
            return 0
        elif amount < 0:
            return -1

        if self.mem[amount] != -2:
            return self.mem[amount]

        res = float('inf')
        for coin in coins:
            count = self.dp(coins,amount - coin)

            if count ==-1:
                continue

            res = min(res, count + 1)

        if res != float('inf'):
            self.mem[amount] = res
        else:
            self.mem[amount] = -1

        return self.mem[amount]