Given a list of coins, Find the min number of coins required to reach a target value
Input:
target = 22
coins = [1 3 4 5]
Output:
5
Taking the Knapsack
we extend it to unbounded knapsack
TC: O(2^n)
SC: O(n) Stack Space 
def min_coins(target, coins):
    def recurse(ind, val):
        if ind == 0:
            if val % coins[ind] == 0:
                return val//coins[ind]
            return math.inf
        take = math.inf
        if coins[ind] <= val:
            take = 1 + recurse(ind, val - coins[ind])
        not_take = recurse(ind - 1, val)
        return min(take, not_take)
    return recurse(len(coins)-1, target)
TC: O(n^2)
SC: O(n^2) + O(n)
def min_coins(target, coins):
    dp = [[-1]*(target+1) for _ in range(len(coins))]
    def recurse(ind, val):
        if ind == 0:
            if val % coins[ind] == 0:
                return val//coins[ind]
            return math.inf
        if dp[ind][val] != -1:
            return dp[ind][val]
        take = math.inf
        if coins[ind] <= val:
            take = 1 + recurse(ind, val - coins[ind])
        not_take = recurse(ind - 1, val)
        dp[ind][val] = min(take, not_take)
        return dp[ind][val]
    return dp[-1][-1] if dp[-1][-1] != math.inf else -1
TC: O(n^2)
SC: O(n^2)
def min_coins(target, coins):
    dp = [[0]*(target+1) for _ in range(len(coins))]
    for val in range(0, target + 1):
        dp[0][val] = val // coins[0] if val % coins[0] == 0 else math.inf
    for ind in range(1, len(coins)):
        for val in range(1, target+1):
            take = math.inf
            if coins[ind] <= val:
                take = 1 + dp[ind][val - coins[ind]]
            
            not_take = dp[ind - 1][val]
            dp[ind][val] = min(take, not_take)
    return dp[-1][-1] if dp[-1][-1] != 0 else -1