07 October 2022

A Hot Tech Interview Topic - Dynamic Programming

Dynamic programming is one of the hot interview topics frequently asked during technical interviews. If you are applying for a software engineering role, this blog will provide you with an overview of dynamic programming and a list of practice problems for you to familiarize yourself with.

To illustrate this, imagine one day your friend comes to you with a large complicated puzzle, which in itself contains multiple smaller puzzles. You, excited to get started, pull up your sleeves and begin solving the smaller puzzles immediately. Not long into this task, you realize you have seen and solved these puzzles before. But you can't remember how you did it previously, or what the solution was. This time will be different! You decide to store the solution of each of the small puzzles you solve, in case you encounter them again. It turned out to be a great idea, as saving the results of the sub-puzzles saved you much time and unnecessary repeating of work. Because of this, you ended up solving the large complicated puzzle in a much shorter timeframe. This is the idea behind dynamic programming; avoid recalculating the same subproblems by storing intermediate results and then re-using the stored results when needed.

Below, I summarized a list of dynamic programming interview questions and answers from Leetcode to help you better understand the topic. Happy coding!😄

https://leetcode.com/problems/house-robber/

class Solution:
  def rob(self, nums: List[int]) -> int:
      n = len(nums)
      
      dp = [0] * (n + 1)
      dp[1] = nums[0]
      
      for i in range(2, n + 1):
          dp[i] = max(dp[i - 1], dp[i - 2] + nums[i - 1])
          
      return dp[n]

https://leetcode.com/problems/house-robber-ii/

class Solution:
  def rob(self, nums: List[int]) -> int:
      def helper(ls):
          n = len(ls)
          
          dp = [0] * (n + 1)
          dp[1] = ls[0]
          
          for i in range(2, n + 1):
              dp[i] = max(dp[i - 1], dp[i - 2] + ls[i - 1])
              
          return dp[n]
      
      if len(nums) == 1: return nums[0]
      return max(helper(nums[1:]), helper(nums[:-1]))

https://leetcode.com/problems/house-robber-iii/

class Solution:
  def rob(self, root: Optional[TreeNode]) -> int:
      def dfs(root):
          if not root: return [0, 0]
          
          l, r = dfs(root.left), dfs(root.right)
          
          ans = [0, 0]
          
          ans[0] = max(l[0], l[1]) + max(r[0], r[1])
          ans[1] = root.val + l[0] + r[0]
          
          return ans
      
      return max(dfs(root))

https://leetcode.com/problems/delete-and-earn/

class Solution:
  def deleteAndEarn(self, nums: List[int]) -> int:
      dp = [0] * 10001

      for x in nums:
          dp[x] += x
      
      for i in range(2, len(dp)):
          dp[i] = max(dp[i - 1], dp[i - 2] + dp[i])
      
      return dp[10000]

https://leetcode.com/problems/pizza-with-3n-slices/

class Solution:
  def maxSizeSlices(self, slices: List[int]) -> int:
      def helper(ls):
          n = len(ls)
          k = (n + 1) // 3
          
          dp = [[0] * (k + 1) for _ in range(n + 1)]
          
          for i in range(1, n + 1):
              for j in range(1, k + 1):
                  dp[i][j] = max(dp[i - 1][j], (dp[i - 2][j - 1] if i - 2 >= 0 else 0) + ls[i - 1])
                  
          return dp[n][k]
          
      return max(helper(slices[1:]), helper(slices[:-1]))

https://leetcode.com/problems/paint-house/

class Solution:
  def minCost(self, costs: List[List[int]]) -> int:
      n = len(costs)
      dp = [[0] * 3 for _ in range(n + 1)]
      
      for i in range(1, n + 1):
          for j in range(3):
              dp[i][j] = min(dp[i - 1][(j + 1) % 3], dp[i - 1][(j + 2) % 3]) + costs[i - 1][j]
                 
      return min(dp[n])

https://leetcode.com/problems/paint-house-ii/

class Solution:
  def minCostII(self, costs: List[List[int]]) -> int:
      n, k = len(costs), len(costs[0])
      
      dp = [[inf] * k for _ in range(n + 1)]
      dp[0] = [0] * k
      
      for i in range(1, n + 1):
          for j in range(k):
              for h in range(k):
                  if j != h:
                      dp[i][j] = min(dp[i][j], dp[i - 1][h] + costs[i - 1][j])
                      
      return min(dp[n])

https://leetcode.com/problems/paint-fence/

class Solution:
  def numWays(self, n: int, k: int) -> int:
      same, diff = 0, k
      
      for i in range(1, n):
          same, diff = diff, (same + diff) * (k - 1)
          
      return same + diff

https://leetcode.com/problems/paint-house-iii/

class Solution:
  def minCost(self, houses: List[int], cost: List[List[int]], m: int, n: int, target: int) -> int:
      dp = [[[inf]*(n+1) for _ in range(target+1)] for _ in range(m+1)]
      dp[0][0] = [0]*(n+1)
      
      for i in range(1, m+1):
          for j in range(1, target+1):
              if houses[i-1] == 0:
                  for k in range(1, n+1):
                      for prek in range(1, n+1):
                          dp[i][j][k] = min(dp[i][j][k], dp[i-1][j-(k!=prek)][prek]+cost[i-1][k-1])
                          
              else:
                  for prek in range(1, n+1):
                      dp[i][j][houses[i-1]] = min(dp[i][j][houses[i-1]], dp[i-1][j-(houses[i-1]!=prek)][prek])
                      
      ans = min(dp[m][target])
      return ans if ans != inf else -1

https://leetcode.com/problems/best-time-to-buy-and-sell-stock/

class Solution:
  def maxProfit(self, prices: List[int]) -> int:
      profit = 0
      buy = prices[0]
      
      for p in prices:
          buy = min(buy, p)
          profit = max(profit, p - buy)
          
      return profit

https://leetcode.com/problems/best-time-to-buy-and-sell-stock-ii/

class Solution:
  def maxProfit(self, prices: List[int]) -> int:
      n = len(prices)
      profit = 0
      
      for i in range(1, n):
          profit += max(0, prices[i] - prices[i - 1]) 
      
      return profit

https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iii/

class Solution:
  def maxProfit(self, prices: List[int]) -> int:
      n = len(prices)
      k = 2
      
      dp = [[[0] * 2 for _ in range(k + 1)] for _ in range(n + 1)]
      
      for i in range(k + 1):
          dp[1][i][1] = -prices[0]
          
      for i in range(2, n + 1):
          for j in range(1, k + 1):
              dp[i][j][0] = max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i - 1])
              dp[i][j][1] = max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i - 1])
              
      return dp[n][k][0]

https://leetcode.com/problems/best-time-to-buy-and-sell-stock-iv/

class Solution:
  def maxProfit(self, k: int, prices: List[int]) -> int:
      n = len(prices)
      
      dp = [[[0] * 2 for _ in range(k + 1)] for _ in range(n + 1)]
      
      for i in range(k + 1):
          dp[1][i][1] = -prices[0]
          
      for i in range(2, n + 1):
          for j in range(1, k + 1):
              dp[i][j][0] = max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i - 1])
              dp[i][j][1] = max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i - 1])
      
      return dp[n][k][0]

https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/

class Solution:
  def maxProfit(self, prices: List[int]) -> int:
      n = len(prices)
      
      dp = [[0] * 2 for _ in range(n + 1)]  
      dp[1][1] = -prices[0]
      
      for i in range(2, n + 1):
          dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1])
          dp[i][1] = max(dp[i - 1][1], dp[i - 2][0] - prices[i - 1])
          
      return dp[n][0]

https://leetcode.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/

class Solution:
  def maxProfit(self, prices: List[int], fee: int) -> int:
      n = len(prices)
      
      dp = [[0] * 2 for _ in range(n + 1)]
      dp[1][1] = -prices[0]
      
      for i in range(2, n + 1):
          dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1] - fee)
          dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i - 1])
          
      return dp[n][0]

https://leetcode.com/problems/maximum-subarray-sum-after-one-operation/

class Solution:
  def maxSumAfterOperation(self, nums: List[int]) -> int:
      n = len(nums)
      dp = [[0] * 2 for _ in range(n + 1)]
      
      dp[1][0] = nums[0]
      dp[1][1] = nums[0] ** 2
      
      ans = dp[1][1]
      
      for i in range(2, n + 1):
          dp[i][0] = max(0, dp[i - 1][0]) + nums[i - 1]
          dp[i][1] = max(dp[i - 1][1] + nums[i - 1], max(0, dp[i - 1][0]) + nums[i - 1] ** 2)
          
          ans = max(ans, dp[i][1])

      return ans

https://leetcode.com/problems/greatest-sum-divisible-by-three/

class Solution:
  def maxSumDivThree(self, nums: List[int]) -> int:
      n = len(nums)
      
      dp = [[0] * 3 for _ in range(n + 1)]
      dp[0][0] = 0
      dp[0][1] = -inf
      dp[0][2] = -inf
      
      for i in range(1, n + 1):
          if nums[i - 1] % 3 == 0:
              dp[i][0] = max(dp[i - 1][0], dp[i - 1][0] + nums[i - 1])
              dp[i][1] = max(dp[i - 1][1], dp[i - 1][1] + nums[i - 1])
              dp[i][2] = max(dp[i - 1][2], dp[i - 1][2] + nums[i - 1])
          elif nums[i - 1] % 3 == 1:
              dp[i][0] = max(dp[i - 1][0], dp[i - 1][2] + nums[i - 1])
              dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + nums[i - 1])
              dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + nums[i - 1])     
          else:
              dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + nums[i - 1])
              dp[i][1] = max(dp[i - 1][1], dp[i - 1][2] + nums[i - 1])
              dp[i][2] = max(dp[i - 1][2], dp[i - 1][0] + nums[i - 1])
      
      return dp[n][0]

https://leetcode.com/problems/count-ways-to-build-good-strings/

class Solution:
  def countGoodStrings(self, low: int, high: int, zero: int, one: int) -> int:
      MOD = 10 ** 9 + 7
      ans = 0
      dp = [0] * 100005
      dp[0] = 1

      for i in range(1, high + 1):
          if i >= zero: dp[i] = dp[i] + dp[i - zero]
          if i >= one: dp[i] = dp[i] + dp[i - one]
          if i >= low: ans = ans + dp[i]

      return ans % MOD

https://leetcode.com/problems/longest-palindromic-substring/

class Solution:
  def longestPalindrome(self, s: str) -> str:
      n = len(s)
      x, y = 0, 0
      dp = [[False] * n for _ in range(n)]
      
      for i in range(n):
          for j in range(i, -1, -1):
              if s[j] == s[i] and (i - j <= 2 or dp[j + 1][i - 1]):
                  dp[j][i] = True
                  if i - j > y - x:
                      x, y = j, i

      return s[x: y + 1]

https://leetcode.com/problems/maximum-number-of-non-overlapping-palindrome-substrings/

class Solution:
  def maxPalindromes(self, s: str, k: int) -> int:
      n = len(s)
      t = []
      dp = [[False] * n for _ in range(n)]

      for i in range(n):
          for j in range(i, -1, -1):
              if s[i] == s[j] and (i - j <= 2 or dp[j + 1][i - 1]):
                  dp[j][i] = True
                  if i - j + 1 >= k: t.append([j, i])
      
      ans = 0
      r = -inf

      for x, y in t:
          if x > r: 
              r = y
              ans += 1
          else:
              r = min(r, y)
      return ans

https://leetcode.com/problems/palindromic-substrings/

class Solution:
  def countSubstrings(self, s: str) -> int:
      n = len(s)
      ans = 0

      dp = [[False] * n for _ in range(n)]

      for i in range(n):
          for j in range(i, -1, -1):
              if s[j] == s[i] and (i - j <= 2 or dp[j + 1][i - 1]):
                  dp[j][i] = True
                  ans += 1

      return ans

https://leetcode.com/problems/palindrome-partitioning-iv/

class Solution:
  def checkPartitioning(self, s: str) -> bool:
      n = len(s)
      dp = [[False] * n for _ in range(n)]

      for i in range(n):
          for j in range(i, -1, -1):
              if s[j] == s[i] and (i - j <= 2 or dp[j + 1][i - 1]):
                  dp[j][i] = True

      for i in range(1, n - 1):
          for j in range(i + 1, n):
              if dp[0][i - 1] and dp[i][j - 1] and dp[j][n - 1]:
                  return True
      return False

https://leetcode.com/problems/palindrome-partitioning/

class Solution:
  def partition(self, s: str) -> List[List[str]]:
      def dfs(u, path):
          if u == n:
              ans.append(path[:])
          for i in range(u,  n):
              if dp[u][i]:
                  dfs(i + 1, path + [s[u: i + 1]])

      n = len(s)
      dp = [[False] * n for _ in range(n)]

      for i in range(n):
          for j in range(i, -1, -1):
              if s[i] == s[j] and (i - j <= 2 or dp[j + 1][i - 1]):
                  dp[j][i] = True

      ans = []
      dfs(0, [])
      return ans

https://leetcode.com/problems/longest-palindromic-subsequence/

class Solution:
  def longestPalindromeSubseq(self, s: str) -> int:
      n = len(s)
      dp = [[0] * (n + 1) for _ in range(n + 1)]

      for i in range(n + 1): dp[i][i] = 1
      for i in range(n + 1):
          for j in range(i - 1, 0, -1):
              if s[j - 1] == s[i - 1]:
                  dp[j][i] = dp[j + 1][i - 1] + 2
              else:
                  dp[j][i] = max(dp[j][i - 1], dp[j + 1][i])
      print(dp)
      return dp[1][n]    

https://leetcode.com/problems/valid-palindrome-iii/

class Solution:
  def isValidPalindrome(self, s: str, k: int) -> bool:
      n = len(s)
      dp = [[0] * (n + 1) for _ in range(n + 1)]

      for i in range(n + 1):
          for j in range(i - 1, 0, -1):
              if s[j - 1] == s[i - 1]:
                  dp[j][i] = dp[j + 1][i - 1]
              else:
                  dp[j][i] = min(dp[j + 1][i], dp[j][i - 1]) + 1

      return dp[1][n] <= k

https://leetcode.com/problems/minimum-insertion-steps-to-make-a-string-palindrome/

class Solution:
  def minInsertions(self, s: str) -> int:
      n = len(s)
      dp = [[0] * (n + 1) for _ in range(n + 1)]

      for i in range(n + 1):
          for j in range(i - 1, 0, -1):
              if s[j - 1] == s[i - 1]:
                  dp[j][i] = dp[j + 1][i - 1]
              else:
                  dp[j][i] = min(dp[j + 1][i], dp[j][i - 1]) + 1
      
      return dp[1][n]

https://leetcode.com/problems/maximize-palindrome-length-from-subsequences/

class Solution:
  def longestPalindrome(self, word1: str, word2: str) -> int:
      ans = 0
      s = word1 + word2
      n = len(s)

      dp = [[0] * (n + 1) for _ in range(n + 1)]
      for i in range(n + 1): dp[i][i] = 1

      for i in range(n + 1):
          for j in range(i - 1, 0, -1):
              if s[j - 1] == s[i - 1]:
                  dp[j][i] = dp[j + 1][i - 1] + 2
                  if j <= len(word1) and i > len(word1): ans = max(ans, dp[j][i])
              else:
                  dp[j][i] = max(dp[j + 1][i], dp[j][i - 1])

      return ans

https://leetcode.com/problems/longest-increasing-subsequence/

class Solution:
  def lengthOfLIS(self, nums: List[int]) -> int:
      dp = []

      for x in nums:
          p = bisect_left(dp, x)
          if p == len(dp): dp.append(x)
          else: dp[p] = x
      
      return len(dp)

https://leetcode.com/problems/increasing-triplet-subsequence/

class Solution:
  def increasingTriplet(self, nums: List[int]) -> bool:
      dp = []

      for x in nums:
          p = bisect_left(dp, x)
          if p == len(dp): dp.append(x)
          else: dp[p] = x
      
      return len(dp) >= 3

https://leetcode.com/problems/number-of-longest-increasing-subsequence/

class Solution:
  def findNumberOfLIS(self, nums: List[int]) -> int:
      n = len(nums)
      dp = [1] * n
      cnt = [1] * (n + 1)

      for i in range(n):
          for j in range(i):
              if nums[i] > nums[j]:
                  if dp[j] + 1 > dp[i]:
                      dp[i] = dp[j] + 1
                      cnt[i] = cnt[j]
                  elif dp[j] + 1 == dp[i]:
                      cnt[i] += cnt[j]
      
      mx = max(dp)
      return sum(cnt[i] for i in range(n) if dp[i] == mx)

https://leetcode.com/problems/russian-doll-envelopes/

class Solution:
  def maxEnvelopes(self, envelopes: List[List[int]]) -> int:
      envelopes.sort(key = lambda x: (x[0], -x[1]))
      dp = []
      
      for x in envelopes:

          l, r = 0, len(dp)
          while l < r:
              mid = (l + r) // 2
              
              if dp[mid][1] >= x[1]: r = mid
              else: l = mid + 1

          if l == len(dp): dp.append(x)
          else: dp[l] = x
      
      return len(dp)

https://leetcode.com/problems/maximum-length-of-pair-chain/

class Solution:
  def findLongestChain(self, pairs: List[List[int]]) -> int:
      pairs.sort(key = lambda x: (x[1], x[0]))
      dp = []

      for x in pairs:
          if not dp or dp[-1][1] < x[0]: dp.append(x)
      
      return len(dp)

https://leetcode.com/problems/edit-distance/

class Solution:
  def minDistance(self, word1: str, word2: str) -> int:
      m = len(word1)
      n = len(word2)
      dp = [[0] * (n + 1) for _ in range(m + 1)]

      for i in range(1, m + 1): dp[i][0] = i
      for i in range(1, n + 1): dp[0][i] = i

      for i in range(1, m + 1):
          for j in range(1, n + 1):
              if word1[i - 1] == word2[j - 1]:
                  dp[i][j] = dp[i - 1][j - 1]
              else:
                  dp[i][j] = min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1
                  
      return dp[m][n]

https://leetcode.com/problems/maximum-length-of-repeated-subarray/

class Solution:
  def findLength(self, nums1: List[int], nums2: List[int]) -> int:
      ans = 0
      m, n = len(nums1), len(nums2)

      dp = [[0] * (n + 1) for _ in range(m + 1)]

      for i in range(1, m + 1):
          for j in range(1, n + 1):
              if nums1[i - 1] == nums2[j - 1]:
                  dp[i][j] = dp[i - 1][j - 1] + 1
              ans = max(ans, dp[i][j])    

      return ans

https://leetcode.com/problems/longest-common-subsequence/

class Solution:
  def longestCommonSubsequence(self, text1: str, text2: str) -> int:
      m = len(text1)
      n = len(text2)
      
      dp = [[ 0 for j in range(n + 1) ] for i 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]

https://leetcode.com/problems/shortest-common-supersequence/

class Solution:
  def shortestCommonSupersequence(self, str1: str, str2: str) -> str:
      m, n = len(str1), len(str2)
      dp = [[0] * (n + 1) for _ in range(m + 1)]

      for i in range(1, m + 1):
          for j in range(1, n + 1):
              if str1[i - 1] == str2[j - 1]:
                  dp[i][j] = dp[i - 1][j - 1] + 1
              else:
                  dp[i][j] = max(dp[i][j - 1], dp[i - 1][j])

      ans = []
      i, j = m, n
      while i >= 1 and j >= 1:
          if dp[i][j] == dp[i - 1][j]:
              ans.append(str1[i - 1])
              i -= 1
          elif dp[i][j] == dp[i][j - 1]:
              ans.append(str2[j - 1])
              j -= 1
          else:
              ans.append(str1[i - 1])
              i -= 1
              j -= 1
              
      while i >= 1:
          ans.append(str1[i - 1])
          i -= 1
      while j >= 1:
          ans.append(str2[j - 1])
          j -= 1
      return ''.join(ans[::-1])

https://leetcode.com/problems/delete-operation-for-two-strings/

class Solution:
  def minDistance(self, word1: str, word2: str) -> int:
      m, n = len(word1), len(word2)
      dp = [[0] * (n + 1) for _ in range(m + 1)]

      for i in range(1, m + 1):
          for j in range(1, n + 1):
              if word1[i - 1] == word2[j - 1]:
                  dp[i][j] = dp[i - 1][j - 1] + 1
              else:
                  dp[i][j] = max(dp[i][j - 1], dp[i - 1][j])
      
      return m + n - 2 * dp[m][n]

https://leetcode.com/problems/minimum-ascii-delete-sum-for-two-strings/

class Solution:
  def minimumDeleteSum(self, s1: str, s2: str) -> int:
      m, n = len(s1), len(s2)
      dp = [[0] * (n + 1) for _ in range(m + 1)]

      for i in range(1, m + 1): dp[i][0] = dp[i - 1][0] + ord(s1[i - 1])
      for j in range(1, n + 1): dp[0][j] = dp[0][j - 1] + ord(s2[j - 1])

      for i in range(1, m + 1):
          for j in range(1, n + 1):
              if s1[i - 1] == s2[j - 1]:
                  dp[i][j] = dp[i - 1][j - 1]
              else:
                  dp[i][j] = min(dp[i - 1][j] + ord(s1[i - 1]), dp[i][j - 1] + ord(s2[j - 1]))

      return dp[m][n]

https://leetcode.com/problems/uncrossed-lines/

class Solution:
  def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int:
      m, n = len(nums1), len(nums2)
      dp = [[0] * (n + 1) for _ in range(m + 1)]

      for i in range(1, m + 1):
          for j in range(1, n + 1):
              if nums1[i - 1] == nums2[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]

https://leetcode.com/problems/max-dot-product-of-two-subsequences/

class Solution:
  def maxDotProduct(self, nums1: List[int], nums2: List[int]) -> int:
      m, n = len(nums1), len(nums2)
      dp = [[-inf] * (n + 1) for _ in range(m + 1)]

      for i in range(1, m + 1):
          for j in range(1, n + 1):
              dp[i][j] = max(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1] + nums1[i - 1] * nums2[j - 1], nums1[i - 1] * nums2[j - 1])

      return dp[m][n]

https://leetcode.com/problems/interleaving-string/

class Solution:
  def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
      m, n = len(s1), len(s2)
      if m + n != len(s3): return False
      
      dp = [[False] * (n + 1) for _ in range(m + 1)]
      dp[0][0] = True

      for i in range(m + 1):
          for j in range(n + 1):
              if i > 0 and s1[i - 1] == s3[i + j - 1]:
                  dp[i][j] |= dp[i - 1][j]
              if j > 0 and s2[j - 1] == s3[i + j - 1]:
                  dp[i][j] |= dp[i][j - 1]

      return dp[m][n]

https://leetcode.com/problems/distinct-subsequences/

class Solution:
  def numDistinct(self, s: str, t: str) -> int:
      m, n = len(s), len(t)
      dp = [[0] * (n + 1) for _ in range(m + 1)]

      for i in range(m + 1): dp[i][0] = 1

      for i in range(1, m + 1):
          for j in range(1, n + 1):
              if i >= j:
                  if s[i - 1] == t[j - 1]:
                      dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]
                  else:
                      dp[i][j] = dp[i - 1][j]

      return dp[m][n]

https://leetcode.com/problems/number-of-ways-to-form-a-target-string-given-a-dictionary/

class Solution:
  def numWays(self, words: List[str], target: str) -> int:
      MOD = 10 ** 9 + 7
      m, n = len(words[0]), len(target)
      cnt = [[0] * 26 for i in range(m)]

      for word in words:
          for i in range(m):
              cnt[i][ord(word[i]) - ord('a')] += 1

      dp = [[0] * (n + 1) for _ in range(m + 1)]

      for i in range(m + 1): dp[i][0] = 1

      for i in range(1, m + 1):
          for j in range(1, n + 1):
              if cnt[i - 1][ord(target[j - 1]) - ord('a')]:
                  dp[i][j] = (dp[i][j] + dp[i - 1][j - 1] * cnt[i - 1][ord(target[j - 1]) - ord('a')] + dp[i - 1][j]) % MOD
              else:
                  dp[i][j] = (dp[i][j] + dp[i - 1][j]) % MOD
      
      return dp[m][n]

https://leetcode.com/problems/coin-change/

class Solution:
  def coinChange(self, coins: List[int], amount: int) -> int:
      dp = [inf] * (amount + 1)
      dp[0] = 0

      for i in range(1, amount + 1):
          for c in coins:
              if i >= c: dp[i] = min(dp[i], dp[i - c] + 1)
      
      return dp[-1] if dp[-1] != inf else -1

https://leetcode.com/problems/coin-change-ii/

class Solution:
  def change(self, amount: int, coins: List[int]) -> int:
      dp = [0] * (amount + 1)
      dp[0] = 1

      for c in coins:
          for i in range(c, amount + 1):
              dp[i] += dp[i - c]

      return dp[-1]

https://leetcode.com/problems/partition-equal-subset-sum/

class Solution:
  def canPartition(self, nums: List[int]) -> bool:
      @cache
      def dp(u, s):
          if u == len(nums):
              return s == tol // 2
          if s > tol // 2: return False 

          return dp(u + 1, s + nums[u]) or dp(u + 1, s)

      tol = sum(nums)
      if tol % 2 == 1: return False
      return dp(0, 0)
class Solution:
  def canPartition(self, nums: List[int]) -> bool:
      n = len(nums)
      if n == 1: return False
      tol = sum(nums)
      if tol % 2 == 1: return False

      V = tol // 2
      dp = [False] * (V + 1)
      dp[0] = True

      for x in nums:
          for i in range(V, x - 1, -1):
              dp[i] |= dp[i - x]

      return dp[V]

https://leetcode.com/problems/target-sum/

class Solution:
  def findTargetSumWays(self, nums: List[int], target: int) -> int: 
      n = len(nums)
      tol = sum(nums)
      if tol < target or (tol - target) % 2 == 1: return 0

      V = (tol - target) // 2
      dp = [0] * (V + 1)
      dp[0] = 1

      for x in nums:
          for i in range(V, x - 1, -1):
              dp[i] += dp[i - x]

      return dp[V]

https://leetcode.com/problems/last-stone-weight-ii/

class Solution:
  def lastStoneWeightII(self, stones: List[int]) -> int:
      tol = sum(stones)
      V = tol // 2
      dp = [0] * (V + 1)

      for x in stones:
          for i in range(V, x - 1, -1):
              dp[i] = max(dp[i], dp[i - x] + x)
      
      return tol - 2 * dp[V]

https://leetcode.com/problems/ones-and-zeroes/

class Solution:
  def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
      dp = [[0] * (n + 1) for _ in range(m + 1)]

      for s in strs:
          zero, one = 0, 0
          for c in s:
              if c == '0':
                  zero += 1
              else:
                  one += 1

          for i in range(m, zero - 1, -1):
              for j in range(n, one - 1, -1):
                  dp[i][j] = max(dp[i][j], dp[i - zero][j - one] + 1)
          
      return dp[m][n]

https://leetcode.com/problems/split-array-with-same-average/

class Solution:
  def splitArraySameAverage(self, nums: List[int]) -> bool:
      @cache
      def dp(u, cnt, sm):
          if cnt == 0: return sm == 0
          if sm < 0 or u + cnt > n: return False
          return dp(u + 1, cnt - 1, sm - nums[u]) or dp(u + 1, cnt, sm)

      n = len(nums)
      tol = sum(nums)

      for i in range(1, (n // 2) + 1):
          if tol * i % n == 0 and dp(0, i, tol * i // n): return True
      return False

https://leetcode.com/problems/minimize-the-difference-between-target-and-chosen-elements/

class Solution:
  def minimizeTheDifference(self, mat: List[List[int]], target: int) -> int:
      dp = set(mat[0])

      for i in range(1, len(mat)):
          nxt = set()
          for x in mat[i]:
              for pre in dp:
                  nxt.add(x + pre)
          dp = nxt

      return min(abs(x - target) for x in dp)

https://leetcode.com/problems/triangle/

class Solution:
  def minimumTotal(self, triangle: List[List[int]]) -> int:
      n = len(triangle)

      for i in range(1, n):
          for j in range(i + 1):
              triangle[i][j] += min(triangle[i - 1][j] if i != j else inf, triangle[i - 1][j - 1] if j != 0 else inf)

      return min(triangle[-1])

https://leetcode.com/problems/minimum-falling-path-sum/

class Solution:
  def minFallingPathSum(self, matrix: List[List[int]]) -> int:
      n = len(matrix)

      for i in range(1, n):
          for j in range(n):
              t = inf
              if j - 1 >= 0: t = min(t, matrix[i - 1][j - 1])
              t = min(t, matrix[i - 1][j])
              if j + 1 < n: t = min(t, matrix[i - 1][j + 1])
              matrix[i][j] += t
      
      return min(matrix[-1])

https://leetcode.com/problems/minimum-falling-path-sum-ii/

class Solution:
  def minFallingPathSum(self, grid: List[List[int]]) -> int:
      n = len(grid)

      for i in range(1, n):
          l, r = grid[i - 1][:], grid[i - 1][:]
          for j in range(1, n): l[j] = min(l[j], l[j - 1])
          for j in range(n - 2, -1, -1): r[j] = min(r[j], r[j + 1])

          for j in range(n):
              grid[i][j] += min(l[j - 1] if j != 0 else inf, r[j + 1] if j != n - 1 else inf)
          
      return min(grid[-1])

https://leetcode.com/problems/out-of-boundary-paths/

class Solution:
  def findPaths(self, m: int, n: int, maxMove: int, startRow: int, startColumn: int) -> int:
      @cache
      def dp(i, j, k):
          if not (0 <= i < m and 0 <= j < n): return 1
          if k == 0: return 0
          ans = 0
          for ni, nj in [(i - 1, j), (i + 1, j), (i, j - 1), (i, j + 1)]:
              ans = (ans + dp(ni, nj, k - 1)) % MOD
          return ans
          
      MOD = 10 ** 9 + 7
      return dp(startRow, startColumn, maxMove)

https://leetcode.com/problems/cherry-pickup/

class Solution:
  def cherryPickup(self, grid: List[List[int]]) -> int:
      @cache
      def dp(i, j, x, y):
          if i == j == n - 1: return grid[i][j]
          cur = grid[i][j] + grid[x][y]
          if i == x and j == y: cur //= 2
          
          ans = -inf
          for ni, nj in [(i + 1, j), (i, j + 1)]:
              if ni < n and nj < n and grid[ni][nj] != -1:
                  for nx, ny in [(x + 1, y), (x, y + 1)]:
                      if nx < n and ny < n and grid[nx][ny] != -1:
                          ans = max(ans, cur + dp(ni, nj, nx, ny))
          return ans

      n = len(grid)
      return max(0, dp(0, 0, 0, 0))