# 2-D Dynamic Programming (11) > 紀錄 NeetCode-150 與 LeetCode 的解題想法、刷題過程與錯誤,以 Python 或 C++ 兩種語言為主 > > 其他 NeetCode-150 或 LeetCode 的主題請見 [Here](https://hackmd.io/-ovTFlzLRWaSgzDGhbSwsA) <!-- 常用色碼紀錄 Easy: <font color="#00ad5f">**Easy**</font> Medium: <font color="#ffbb00">**Medium**</font> Hard: <font color="#ee2f56">**Hard**</font> --> 所謂的 2-D dynamic programming 看起來不只是 2-D 的 memorization array,而是在 DP 的遞迴中有兩個控制參數。 ## 1. Unique Paths <font color="#ffbb00">**Medium**</font> > There is an `m x n` grid where you are allowed to move either down or to the right at any point in time. > > Given the two integers `m` and `n`, return the number of possible unique paths that can be taken from the top-left corner of the grid (`grid[0][0]`) to the bottom-right corner (`grid[m - 1][n - 1]`). > > You may assume the output will fit in a 32-bit integer. ### Example 1: ![image](https://hackmd.io/_uploads/Sk79Uaro1g.png) ```java Input: m = 3, n = 6 Output: 21 ``` ### Example 2: ```java Input: m = 3, n = 3 Output: 6 ``` ### Constraints * `1 <= m, n <= 100` ### Solution 排列組合的加法原理 ```pthon= class Solution: def uniquePaths(self, m: int, n: int) -> int: if m == 1 and n == 1: return 1 M = [[0 for _ in range(n)] for _ in range(m)] # bounary for r in range(m): if r == 0: for c in range(1, n): M[r][c] = 1 else: M[r][0] = 1 for r in range(1, m): for c in range(1, n): M[r][c] = M[r][c-1] + M[r-1][c] return M[m-1][n-1] ``` ### Complexity * Time complexity: $O(m \cdot n)$ * Space complexity: $O(m \cdot n)$ ## 2. Longest Common Subsequence <font color="#ffbb00">**Medium**</font> > Given two strings `text1` and `text2`, return the length of the longest common subsequence between the two strings if one exists, otherwise return `0`. > > A subsequence is a sequence that can be derived from the given sequence by deleting some or no elements without changing the relative order of the remaining characters. > > * For example, `"cat"` is a subsequence of `"crabt"`. > > A **common subsequence** of two strings is a subsequence that exists in both strings. ### Example 1: ```java Input: text1 = "cat", text2 = "crabt" Output: 3 ``` Explanation: The longest common subsequence is "cat" which has a length of 3. ### Example 2: ```java Input: text1 = "abcd", text2 = "abcd" Output: 4 ``` ### Example 3: ```java Input: text1 = "abcd", text2 = "efgh" Output: 0 ``` ### Constraints * `1 <= text1.length, text2.length <= 1000` * `text1` and `text2` consist of only lowercase English characters. ### Solution 將兩字串中的字元依序比對,依序對應字元是否相同可拆解成兩種不同的子問題 * 若某字元相同,則同時往後一個繼續比較 substring/subproblem * 若某字元不同,則彼此往後一個交叉比較 substring/subproblem 以 Example 1 為例,比較 `"s1 = cat"` 與 `"s2 = crabt"` 兩字串 * `s1[0] = 'c' == 'c' = s2[0]` * index = 0 時對應字元相同,繼續比較 index 1 至結束 * 比較子問題 `s1[1:]` 與 `s2[1:]` * `s1[1] = 'a' != 'r' = s2[1]` * index = 1 時對應字元不同,交叉比較 substring/subproblem * 比較子問題 `s1[1:]` 與 `s2[2:]` * 比較子問題 `s1[2:]` 與 `s1[1:]` 為了實現子問題的,可將上述子問題的拆解以二維矩陣視覺化 ![image](https://hackmd.io/_uploads/rJRVApHiye.png) * 當對應字元相同,兩者同時退一位,所以會往右下的對角線(綠箭頭)繼續找 subproblem * 當對應字元不同,兩者彼此退一位交叉比較,所以會往右或下(藍箭頭)繼續找 subproblem * 以棕色方框的 No 為例,就是比較 `"bat"` 與 `"t"` 兩個 subproblem,且因為 `'b' != 't'`,所以往右或下繼續比較 因為 DP 就是找數學歸納法的步驟,以此題為例,當字元相同時代表該位置有貢獻,需查看他的前一步貢獻(最長子序列)是多少,因此必須從末端往回推(紅色箭頭) * 該位置有貢獻(1),找前一個對角線位置的值 + 1 * 該位置沒貢獻(No),找右或下的表格取最大值 ```python= class Solution: def longestCommonSubsequence(self, text1: str, text2: str) -> int: ROWS = len(text1) COLS = len(text2) M = [[0 for _ in range(COLS + 1)] for _ in range(ROWS + 1)] for r in range(ROWS - 1, -1, -1): for c in range(COLS - 1, -1, -1): if text1[r] == text2[c]: M[r][c] = 1 + M[r+1][c+1] else: M[r][c] = max(M[r+1][c], M[r][c+1]) return M[0][0] ``` ### Complexity * Time complexity: $O(m \cdot n)$ * Space complexity: $O(m \cdot n)$ ## 3. Best Time to Buy and Sell Stock with Cooldown <font color="#ffbb00">**Medium**</font> > You are given an integer array `prices` where `prices[i]` is the price of NeetCoin on the `ith` day. > > You may buy and sell one NeetCoin multiple times with the following restrictions: > > * After you sell your NeetCoin, you cannot buy another one on the next day (i.e., there is a cooldown period of one day). > * You may only own at most one NeetCoin at a time. > > You may complete as many transactions as you like. > > Return the **maximum profit** you can achieve. ### Example 1: ```java Input: prices = [1,3,4,0,4] Output: 6 ``` Explanation: Buy on day 0 (price = 1) and sell on day 1 (price = 3), profit = 3-1 = 2. Then buy on day 3 (price = 0) and sell on day 4 (price = 4), profit = 4-0 = 4. Total profit is 2 + 4 = 6. ### Example 2: ```java Input: prices = [1] Output: 0 ``` ### Constraints * `1 <= prices.length <= 5000` * `0 <= prices[i] <= 1000` ### Solution * 每個時間點(index = i)都會有一種狀態 buy 或 sell * 每個時間點(index = i)可以選擇下一刻的狀態,但須滿足下列限制 * 若 `(i, buy)`,則下一刻可以是 `(i + 1, sell)` 或 `(i + 1, cooldown)` * 若 `(i, sell)`,則下一刻只能是 `(i + 2, buy)`(因為賣出隔天不能買)或 `(i + 1, cooldown)` * cooldown 等同於狀態不變,持續前一刻的狀態 * 每個時間點有兩種選擇,選擇可產生最大利潤(prifit)的狀態 * 當選擇 index = i 的時間選擇 sell 表示賣出股票,收入 `+ prices[i]` * 當選擇 index = i 的時間選擇 buy 表示買入股票,收入 `- prices[i]` 根據上述規則可以視覺化樹狀圖如下 ![image](https://hackmd.io/_uploads/H1UHASvoyl.png) ```python= class Solution: def maxProfit(self, prices: List[int]) -> int: # True/False for buy/sell M = {} # (i-th day, buy/sell): profit def dp(i, buying): if i >= len(prices): return 0 if (i, buying) in M: return M[(i, buying)] if buying: # buy on i-th day profit = dp(i + 1, not buying) - prices[i] # sell on next day cooldown = dp(i + 1, buying) # cooldown on next day profit = max(profit, cooldown) else: # sell on i-th day profit = dp(i + 2, not buying) + prices[i] # buy on the day after tomorrow cooldown = dp(i + 1, buying) # cooldown on next day profit = max(profit, cooldown) M[(i, buying)] = profit return M[(i, buying)] return dp(0, True) ``` ### Complexity * Time complexity: $O(n)$ * Space complexity: $O(n)$ ## 4. Coin Change ll <font color="#ffbb00">**Medium**</font> > You are given an integer array `coins` representing coins of different denominations (e.g. 1 dollar, 5 dollars, etc) and an integer `amount` representing a target amount of money. > > Return the number of distinct combinations that total up to `amount`. If it's impossible to make up the amount, return `0`. > > You may assume that you have an unlimited number of each coin and that each value in `coins` is unique. ### Example 1: ```java Input: amount = 4, coins = [1,2,3] Output: 4 ``` Explanation: * 1+1+1+1 = 4 * 1+1+2 = 4 * 2+2 = 4 * 1+3 = 4 ### Example 2: ```java Input: amount = 7, coins = [2,4] Output: 0 ``` ### Constraints * `1 <= coins.length <= 100` * `1 <= coins[i] <= 1000` * `0 <= amount <= 1000` ### Solution #### 1. Basic * 避免重複選取,可以先對 coins 做排序,使得可行解以遞增的方排排序 * 為了避免重複選取,硬幣也是以遞增的方式選取 * Ex. 某個時間點選 coins[i] 硬幣,則下一輪選取的時候也只能從 coins[i] 開始選擇,不能再拿 coins[i-1] 以上述規則畫出的樹狀圖如下 ![image](https://hackmd.io/_uploads/ryNrI8Djkg.png) 這種方式的 DP 遞迴會有兩個參數,記憶矩陣也會是二維矩陣,因為要同時紀錄當下的剩餘金額以及從第 i 個硬幣開始選取。 ```python= class Solution: def change(self, amount: int, coins: List[int]) -> int: coins.sort() M = [[-1 for _ in range(len(coins))] for _ in range(amount + 1)] def dp(amt, startIdx): if amt == 0: M[amt][startIdx] = 1 return M[amt][startIdx] if M[amt][startIdx] == -1: res = 0 for i in range(startIdx, len(coins)): if amt - coins[i] >= 0: res += dp(amt - coins[i], i) M[amt][startIdx] = res return M[amt][startIdx] return dp(amount, 0) ``` 針對每一個剩餘金額,選取硬幣時都要迭代整個 coins,所以時間複雜度為 $O(m \cdot n^2)$,其中 m = amount,n = len(coins) #### 2. Improvement 改善方式可以將硬幣選擇的迭代改為二選一。每個時間點將硬幣的選擇分為兩種狀況 * 選擇 coins[i] 硬幣 * 不選擇 coins[i] 硬幣,改為 coins[i+1] 硬幣 ```python= class Solution: def change(self, amount: int, coins: List[int]) -> int: coins.sort() M = [[-1 for _ in range(len(coins))] for _ in range(amount + 1)] def dp(amt, startIdx): if amt == 0: return 1 if startIdx >= len(coins): return 0 if M[amt][startIdx] != -1: return M[amt][startIdx] res = 0 if amt >= coins[startIdx]: res = dp(amt - coins[startIdx], startIdx) # use this coin res += dp(amt, startIdx + 1) # use next coin M[amt][startIdx] = res return M[amt][startIdx] return dp(amount, 0) ``` ### Complexity * Time complexity: $O(m \cdot n)$ * m is amount * n is length of coins * Space complexity: $O(m \cdot n)$ ## 5. Target Sum <font color="#ffbb00">**Medium**</font> > You are given an array of integers `nums` and an integer `target`. > > For each number in the array, you can choose to either add or subtract it to a total sum. > * For example, if `nums = [1, 2]`, one possible sum would be `"+1-2=-1"`. > > If `nums=[1,1]`, there are **two different ways** to sum the input numbers to get a sum of `0`: `"+1-1"` and `"-1+1"`. > > Return the number of different ways that you can build the expression such that the total sum equals `target`. ### Example 1: ```java Input: nums = [2,2,2], target = 2 Output: 3 ``` Explanation: There are 3 different ways to sum the input numbers to get a sum of 2. `+2 +2 -2 = 2` `+2 -2 +2 = 2` `-2 +2 +2 = 2` ### Constraints * `1 <= nums.length <= 20` * `0 <= nums[i] <= 1000` * `-1000 <= target <= 1000` ### Solution nums 中的每個元素都有兩種可能,把所有可能解列出如下,當有相同和與相同選擇時再輔以 DP 解。要同時紀錄當下的總和以及目前輪到哪個 nums 中的哪個元素做選擇,所以 DP 的遞迴函式會有兩個參數,同樣的記憶矩陣也是二維矩陣 len(nums) * (a * target + 1)。 ![image](https://hackmd.io/_uploads/BJeyp8Pike.png) 但如果以矩陣的方式儲存已經出現過的值,可能會超出矩陣的 index,因為當前總和可能 < 0,但矩陣的 index 不會 < 0。若是用矩陣儲存出現過的值,當 index < 0 時需要使用偏移(offset)做修正,但會比較麻煩。 可以改以 hash map 的方式將對應的 (i, currentSum):count 儲存。 ```python= class Solution: def findTargetSumWays(self, nums: List[int], target: int) -> int: M = {} # (index, currentSum) def dp(i, currentSum): if i == len(nums): return 1 if currentSum == target else 0 if (i, currentSum) in M: return M[(i, currentSum)] M[(i, currentSum)] = dp(i + 1, currentSum + nums[i]) + dp(i + 1, currentSum - nums[i]) return M[(i, currentSum)] return dp(0, 0) ``` ### Complexity * Time complexity: $O(m \cdot n)$ * m is sum og nums * n is length of nums * Space complexity: $O(m \cdot n)$ ## 6. Interleaving String <font color="#ffbb00">**Medium**</font> > You are given three strings `s1`, `s2`, and `s3`. Return `true` if `s3` is formed by interleaving `s1` and `s2` together or false otherwise. > > **Interleaving** two strings `s` and `t` is done by dividing `s` and `t` into `n` and `m` substrings respectively, where the following conditions are met > > * `|n - m| <= 1`, i.e. the difference between the number of substrings of `s` and `t` is at most `1`. > * `s = s1 + s2 + ... + sn` > * `t = t1 + t2 + ... + tm` > * **Interleaving** `s` and `t` is `s1 + t1 + s2 + t2 + ...` or `t1 + s1 + t2 + s2 + ...` > > You may assume that `s1`, `s2` and `s3` consist of lowercase English letters. ### Example 1: ![image](https://hackmd.io/_uploads/rkpl7rjjJg.png) ```java Input: s1 = "aaaa", s2 = "bbbb", s3 = "aabbbbaa" Output: true ``` Explanation: We can split `s1` into `["aa", "aa"]`, `s2` can remain as `"bbbb"` and `s3` is formed by interleaving `["aa", "aa"]` and `"bbbb"`. ### Example 2: ```java Input: s1 = "", s2 = "", s3 = "" Output: true ``` ### Example 3: ```java Input: s1 = "abc", s2 = "xyz", s3 = "abxzcy" Output: false ``` Explanation: We can't split `s3` into `["ab", "xz", "cy"]` as the order of characters is not maintained. ### Constraints * `0 <= s1.length, s2.length <= 50` * `0 <= s3.length <= 100` ### Solution 以變數 `i` 控制字串 `s1`,變數 `j` 控制字串 `s2`,變數 `k` 控制字串 `s3`。理論上要三個變數來控制三個字串中目前各自正在處理的字元,但因為 `s3` 是由 `s1` 與 `s2 `交錯組合形成,所以 `s3` 的控制變數 `k` 可以由 `s1` 的 `i` 與 `s2` 的 `j` 相加得到。 以 Example 1 為例: * 當 `i = 2` 時表示正在處理 `s1[3] = 2` * 當 `j = 1` 時表示正在處理 `s2[1] = b` * 此時正在處理目標字串 `s3` 的 `s3[4] = b`,因此 `k = i + j` #### 1. Recursion ver. 每次移動指標 `i` 或 `j` 讓字串 `s1` 或 `s2` 跟 `s3` 配對,如果可以配對成功表示這個字元可以匹配,不行表示需要換另一個字串,可列樹狀圖如下: ![image](https://hackmd.io/_uploads/H1nj5SsjJx.png) 每次可選擇往 `i` 或 `j` 前進,如果可以繼續往下走,則該點回傳 `True`,反之回傳 `False`。 ```python= # Recursion ver. class Solution: def isInterleave(self, s1: str, s2: str, s3: str) -> bool: if len(s1) + len(s2) != len(s3): return False M = {} # (i, j):True/False where i for s1 and j for s2 def dp(i, j): # base condition if i == len(s1) and j == len(s2): return True # memorization if (i, j) in M: return M[(i, j)] if i < len(s1) and s1[i] == s3[i+j] and dp(i + 1, j): M[(i, j)] = True return M[(i, j)] if j < len(s2) and s2[j] == s3[i+j] and dp(i, j + 1): M[(i, j)] = True return M[(i, j)] M[(i, j)] = False return M[(i, j)] return dp(0, 0) ``` #### 2. Ieration ver. (1) 迭代版本表格如下。基礎條件是當兩個字串當處理完成(右下角)時回傳 `True`。 (2) 每一個 cell 去分別對應目前剩餘的 `s1` 字串與 `s2` 字串。 如果 `s1` 配對成功(True),往下走繼續配對;如果 `s2` 配對成功,往右走繼續配對。 (3) 某一個 cell 能否配對成功應該要看他的下一個位置(右/下),如果右或下有其中一個為 True,表示該條路徑可行(True),如果右/下都為 True,表示該條路徑不可行(True)。因此需要從字串尾端逆推回來。 ![image](https://hackmd.io/_uploads/rkEgRSjiJg.png) ```python= # Iteration ver. class Solution: def isInterleave(self, s1: str, s2: str, s3: str) -> bool: if len(s1) + len(s2) != len(s3): return False M = [[False for _ in range(len(s2) + 1)] for _ in range(len(s1) + 1)] M[len(s1)][len(s2)] = True for i in range(len(s1), -1, -1): for j in range(len(s2), -1, -1): # check bottom if i < len(s1) and s1[i] == s3[i+j] and M[i+1][j]: M[i][j] = True # check right if j < len(s2) and s2[j] == s3[i+j] and M[i][j+1]: M[i][j] = True return M[0][0] ``` ### Complexity * Time complexity: $O(m \times n)$ * m is the length of s1 * n is the length of s2 * Space complexity: $O(m \times n)$ ## 7. Longest Increasing Path in Matrix <font color="#ee2f56">**Hard**</font> > You are given a 2-D grid of integers `matrix`, where each integer is greater than or equal to `0`. > > Return the length of the longest strictly increasing path within `matrix`. > > From each cell within the path, you can move either horizontally or vertically. You **may not** move **diagonally**. ### Example 1: ![image](https://hackmd.io/_uploads/H1R1ZLsjJl.png) ```java Input: matrix = [[5,5,3],[2,3,6],[1,1,1]] Output: 4 ``` Explanation: The longest increasing path is `[1, 2, 3, 6]` or `[1, 2, 3, 5]`. ### Example 2: ![image](https://hackmd.io/_uploads/BJrzW8joye.png) ```java Input: matrix = [[1,2,3],[2,1,4],[7,6,5]] Output: 7 ``` Explanation: The longest increasing path is `[1, 2, 3, 4, 5, 6, 7]`. ### Constraints * `1 <= matrix.length, matrix[i].length <= 100` ### Solution 以暴力解來看,每個 cell 都有 4 個方向可走,整個矩陣會有 $m \times n$ 個 cells,因此複雜度為 $4^{m \times n}$。又因為每個 cell 都可能當作起點出發找最長路徑,因此總時間複雜度為 $m \times n \times 4^{m \times n}$。 但實際上,如果使用 DP 後加入 memorization 表格,因為每個從每個點出發的最長路徑都會被紀錄(有計算過的話),所以暴力解的複雜度可以降至 $O(m \times n)$。 遞迴循環中,符合條件的情況(以下) * 不超過 memorization table 的 index 大小 * 下一個 cell 的值 > 目前 cell 的值 才能往前,否則該點不能行走,回傳路徑長為 `0`。每個點的四個方向都要迭代一次。因為要紀錄目前 cell 的值,所以遞迴函式需要多一個參數傳遞。 ```python= class Solution: def longestIncreasingPath(self, matrix: List[List[int]]) -> int: ROWS = len(matrix) COLS = len(matrix[0]) M = [[-1 for _ in range(COLS)] for _ in range(ROWS)] def dp(r, c, currentValue): if (r < 0 or r >= ROWS or c < 0 or c >= COLS or matrix[r][c] <= currentValue): return 0 if M[r][c] != -1: return M[r][c] res = 1 directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] for dr, dc in directions: newR, newC = r + dr, c + dc res = max(res, 1 + dp(newR, newC, matrix[r][c])) M[r][c] = res return M[r][c] res = 0 for r in range(ROWS): for c in range(COLS): dp(r, c, -1) res = max(res, M[r][c]) return res ``` ### Complexity * Time complexity: $O(m \times n)$ * m is the row of matrix * n is the column of matrix * Space complexity: $O(m \times n)$ ## 8. Distinct Subsequence <font color="#ee2f56">**Hard**</font> > You are given two strings `s` and `t`, both consisting of english letters. > > Return the number of distinct **subsequences** of `s` which are equal to `t`. ### Example 1: ```java Input: s = "caaat", t = "cat" Output: 3 ``` Explanation: Rhere are 3 ways you can generate `"cat"` from `s`. * (c)aa(at) * (c)a(a)a(t) * (ca)aa(t) ### Example 2: ```java Input: s = "xxyxy", t = "xy" Output: 5 ``` Explanation: There are 5 ways you can generate `"xy"` from `s`. * (x)x(y)xy * (x)xyx(y) * x(x)(y)xy * x(x)yx(y) * xxy(x)(y) ### Constraints * `1 <= s.length, t.length <= 1000` * `s` and `t` consist of English letters. ### Solution 類似 Longest Common Subsequence 的概念。將字串 s 與字串 t 逐一比較: * 若某字元配對成功,則兩者同時往前推進 * if `s[i] = t[j]` then `s[i+1] and t[j+1]` * 若某字元配對失敗,則只有字串 s 往前推進 * else `s[i+1]` * 配對成功時,因為後面可能會有重複字元出現,所以也可以選擇只推進字串 `s` * 配對到底後,若字串 `t` 為空字串,表示 subsequence 配對成功,回傳一次 ![S__2555909](https://hackmd.io/_uploads/B14TQP3okx.jpg) ```python= class Solution: def numDistinct(self, s: str, t: str) -> int: M = [[-1 for _ in range(len(t))] for _ in range(len(s))] def dp(i, j): if j == len(t): return 1 if i == len(s): return 0 if M[i][j] != -1: return M[i][j] if s[i] == t[j]: M[i][j] = dp(i + 1, j + 1) + dp(i + 1, j) else: M[i][j] = dp(i + 1, j) return M[i][j] return dp(0, 0) ``` ### Complexity * Time complexity: $O(m \times n)$ * m is the length of s * n is the length of t * Space complexity: $O(m \times n)$ ## 9. Edit Distance <font color="#ffbb00">**Medium**</font> > You are given two strings `word1` and `word2`, each consisting of lowercase English letters. > > You are allowed to perform three operations on `word1` an unlimited number of times: > > * Insert a character at any position > * Delete a character at any position > * Replace a character at any position > > Return the minimum number of operations to make `word1` equal `word2`. ### Example 1: ```java Input: word1 = "monkeys", word2 = "money" Output: 2 ``` Explanation: `monkeys` -> `monkey` (remove `s`) `monkey` -> `monkey` (remove `k`) ### Example 2: ```java Input: word1 = "neatcdee", word2 = "neetcode" Output: 3 ``` Explanation: `neatcdee` -> `neetcdee` (replace `a` with `e`) `neetcdee` -> `neetcde` (remove last `e`) `neetcde` -> `neetcode` (insert `o`) ### Constraints * `0 <= word1.length, word2.length <= 100` * `word1` and `word2` consist of lowercase English letters. ### Solution #### 1. Recursion ver. (1) Base condition * 當 `word1 = ""` 且 `word2 = "acde"` * 此時 `word1` 需要操作 4 次的 insert * 當 `word1 = "abc"` 且 `word2 = ""` * 此時 `word1` 需要操作 3 次的 delete 綜上所述,當某一個字串長度 = 0 時,操作次數 = 另一個非空字串的長度 (2) 類似 Longest Common Subsequence(LCS) 兩個字串逐一比較字元, * 當字元相同時,兩者往前推進一位 * if `w1[i] = w2[j]` then (i+1, j+1) * 當字元不同時,有三種操作 * insert: (i, j+1) * 在 `word1` 強制插入與 `word2` 相同的字元,使得 `word2` 推進但 `word1` 不動 * remove: (i+1, j) * 在 `word1` 強制移除不同的字元,使得 `word1` 推進但 `word2` 不動 * replace: (i+1, j+1) * 強制在 `word1` 中替換一個與 `word2` 相同的字元,使得兩者都推進 * 三種操作中選一個操作次數最少的 ![S__2564098](https://hackmd.io/_uploads/HJNwuP3jkx.jpg) ```python= class Solution: def minDistance(self, word1: str, word2: str) -> int: m = len(word1) n = len(word2) M = [[-1 for _ in range(n)] for _ in range(m)] def dp(i, j): if i == m: return n - j if j == n: return m - i if M[i][j] != -1: return M[i][j] if word1[i] == word2[j]: M[i][j] = dp(i+1, j+1) else: M[i][j] = 1 + min(dp(i, j+1), dp(i+1, j), dp(i+1, j+1)) return M[i][j] return dp(0, 0) ``` #### 2. Iteration ver. 在表格中,三種操作剛好對應 cell 往三個方向(右/下/對角)移動。往每一個 cell 的最小操作次數受到兩個影響: * 對應字串相同,直接看對角線的前一步驟的最小值 * 對應字串不同,取三個方向的最小值,再 + 上 cell 本身的一次操作 ![image](https://hackmd.io/_uploads/rkFUuw3iJe.png) ```python= class Solution: def minDistance(self, word1: str, word2: str) -> int: m, n = len(word1), len(word2) M = [[-1 for _ in range(n + 1)] for _ in range(m + 1)] # boundary for i in range(m + 1): M[i][n] = m - i for j in range(n + 1): M[m][j] = n - j for i in range(m - 1, -1, -1): for j in range(n - 1, -1, -1): if word1[i] == word2[j]: M[i][j] = M[i+1][j+1] else: M[i][j] = 1 + min(M[i+1][j], M[i][j+1], M[i+1][j+1]) return M[0][0] ``` ### Complexity * Time complexity: $O(m \times n)$ * m is the length of word1 * n is the length of word2 * Space complexity: $O(m \times n)$ ## 10. Burst Balloons <font color="#ee2f56">**Hard**</font> > You are given an array of integers `nums` of size `n`. The `ith` element represents a balloon with an integer value of `nums[i]`. You must burst all of the balloons. > > If you burst the `ith` balloon, you will receive `nums[i - 1] * nums[i] * nums[i + 1]` coins. If `i - 1` or `i + 1` goes out of bounds of the array, then assume the out of bounds value is 1. > > Return the maximum number of coins you can receive by bursting all of the balloons. ### Example 1: ```java Input: nums = [4,2,3,7] Output: 167 Explanation: nums = [4,2,3,7] --> [4,3,7] --> [4,7] --> [7] --> [] coins = 4*2*3 + 4*3*7 + 1*4*7 + 1*7*1 = 143 ``` ### Constraints * `n == nums.length` * `1 <= n <= 300` * `0 <= nums[i] <= 100` ### Recommended complexity * Time complexity: $O(n^3)$ * Space complexity: $O(n^2)$ ### Solution (1) subproblem 的選擇(False) 不能將 array 分解。以 Example 1 的 `nums = [4, 2, 3, 7]` 為例,當選擇爆破 `nums[2] = 3` 後可將原始陣列分為 `leftArray = [4, 2]` 與 `rightArray = [7]`。而 `leftArray = [4, 2]` 中可以再選擇 2,可得 `coins = 4 * 2 * 1`。 但實際上 `leftArray` 與 `rightArray` 應該是要合併的,選擇 2 的結果應該是 `4 * 2 * 8` 才對。 (2) subproblem 的選擇(True) 反過來思考,如果選擇的數字不是先刪除而是最後刪除。同樣以 Example 1 的 `nums = [4, 2, 3, 7]` 為例,若選擇 `nums[2] = 3` 為最後刪除,則對 3 而言的 `coins = 1 * 3 * 1`。它的前一步(subproblem)是 `leftArray = [4, 2]` 和 `rightArray = [7]`,且兩者不為有合併的問題(因為 `nums[2] = 3` 可作為兩者分界)。 <img src="https://hackmd.io/_uploads/HkcXJlCjkl.jpg" width=300> 因此,可先將 `nums = [4, 2, 3, 7]` 擴充為 `nums = [1, 4, 2, 3, 7, 1]`。假設 `nums[2] = 2` 為最後刪除,則它的貢獻以及子問題為: `nums[L-1] * 2 * nums[R+1] + dp[L][i-1] + dp[i+1][R]`。 <img src="https://hackmd.io/_uploads/B1DN1x0s1l.jpg" width=600> 依此類推,每次迭代 subproblem 中的所有元素作為可能被最後刪除的元素,再取最大值。 ```python= class Solution: def maxCoins(self, nums: List[int]) -> int: nums = [1] + nums + [1] M = [[-1 for _ in range(len(nums))] for _ in range(len(nums))] def dp(L, R): if L > R: return 0 if M[L][R] != -1: return M[L][R] res = 0 for i in range(L, R + 1): coins = nums[L-1] * nums[i] * nums[R+1] + \ dp(L, i - 1) + \ dp(i + 1, R) res = max(res, coins) M[L][R] = res return M[L][R] return dp(1, len(nums) - 2) ``` ## 11. Regular Expression Matching <font color="#ee2f56">**Hard**</font> > You are given an input string `s` consisting of lowercase english letters, and a pattern `p` consisting of lowercase english letters, as well as `'.'`, and `'*'` characters. > > Return `true` if the pattern matches the **entire** input string, otherwise return `false`. > > * `'.'` Matches any single character > * `'*'` Matches zero or more of the preceding element. ### Example 1: ```java Input: s = "aa", p = ".b" Output: false ``` Explanation: Regardless of which character we choose for the `'.'` in the pattern, we cannot match the second character in the input string. ### Example 2: ```java Input: s = "nnn", p = "n*" Output: true ``` Explanation: `'*'` means zero or more of the preceding element, `'n'`. We choose `'n'` to repeat three times. ### Example 3: ```java Input: s = "xyz", p = ".*z" Output: true ``` Explanation: The pattern `".*"` means zero or more of any character, so we choose `".."` to match `"xy"` and `"z"` to match `"z"`. ### Constraint * `1 <= s.length <= 20` * `1 <= p.length <= 20` * Each appearance of `'*'`, will be preceded by a valid character or `'.'`. ### Recommended complexity * Time complexity: as good or better than $O(m \cdot n)$ * m is the length of string s * n is the length of string p * Space complexity: as good or better than $O(m \cdot n)$ ### Solution 由字串 `p` 去配對字串 `s`,記憶化陣列 `M[i][j]` 表示 `s[i:]` 與 `p[j:]` 是否匹配。每個位置會有以下三種配對狀況: * 若 `p[j]` 是字母 * 若 `p[j] = s[i]`,則兩者都推進一位: `(i+1, j+1)` * 若 `p[j] != s[i]`,則該位置不匹配: False * 若 `p[j]` 是 `"."`: 同上第一個,兩者推進一位 `(i+1, j+1)` * 若 `p[j]` 是 `"*"` 有兩種選擇(注意 `"*"` 不能單獨存在,要與前面的字元一起作用) * 不重複前一個,只推進 p: `(i, j+2)` * 重複前面的,但前提是前一個要配對成功,因為如果前一個配對失敗那重複也沒意義: `(i+1, j)` 可繪製樹狀圖如下 ![image](https://hackmd.io/_uploads/Hk8KVgCo1l.png) Base condition: * 如果最後 `i >= len(s)` 且 `j >= len(p)` 表示字串 `s` 與 `p` 走完: `True` * 如果 `j >= len(p)` 表示 s 還沒匹配完就結束: `False` * 如果 `i >= len(s)` 表示 `s` 配對完: `True` ```python= class Solution: def isMatch(self, s: str, p: str) -> bool: M = {} def dp(i, j): if i >= len(s) and j >= len(p): return True if (i, j) in M: return M[(i, j)] if j >= len(p): return False match = i < len(s) and (s[i] == p[j] or p[j] == '.') if (j + 1) < len(p) and p[j+1] == '*': M[(i, j)] = dp(i, j + 2) or (match and dp(i + 1, j)) return M[(i, j)] if match: M[(i, j)] = dp(i+1, j+1) return M[(i, j)] M[(i, j)] = False return M[(i, j)] return dp(0, 0) ```