題意
有 n n n 個氣球,編號從 0 0 0 到 n − 1 n - 1 n − 1 。給定一個陣列 n u m s nums n u m s 表示每個氣球上的數字。你的任務是戳破所有的氣球。
如果你戳破第 i i i 個氣球,你會得到 n u m s [ i − 1 ] × n u m s [ i ] × n u m s [ i + 1 ] nums[i - 1] \times nums[i] \times nums[i + 1] n u m s [ i − 1 ] × n u m s [ i ] × n u m s [ i + 1 ] 的硬幣。如果 i − 1 i - 1 i − 1 或 i + 1 i + 1 i + 1 超出陣列的範圍,那麼就把它們當作有一個寫著 1 1 1 的氣球。
返回通過戳破氣球可以收集到的最多硬幣數量。
思路:倒序戳氣球 + 動態規劃(DP)
由於會存在只剩下一個氣球的情況,所以我們在陣列的頭尾加上 1 1 1 ,這樣就可以確保每個氣球的左右邊界都有值。
由於按照順序戳氣球需要維護氣球的狀態,所以我們可以 倒序 戳氣球,這樣就不需要維護氣球的狀態。假設在範圍 [ l , r ] [l, r] [ l , r ] 中,最後一個戳破的氣球是第 i i i 個氣球,那麼我們可以將問題分成兩個子問題:
戳破第 i i i 個氣球左邊的所有氣球,即問題變成戳破 [ l , i ] [l, i] [ l , i ] 中的氣球
戳破第 i i i 個氣球右邊的所有氣球,即問題變成戳破 [ i , r ] [i, r] [ i , r ] 中的氣球
因此可以令 f ( l , r ) f(l, r) f ( l , r ) 表示戳破 [ l , r ] [l, r] [ l , r ] 中的氣球所能獲得的最多硬幣數量,則可以寫出以下轉移式:
f ( l , r ) = max l < i < r { f ( l , i ) + f ( i , r ) + nums [ l ] × nums [ i ] × nums [ r ] } f(l, r) = \max\limits_{l < i < r} \{f(l, i) + f(i, r) + \text{nums}[l] \times \text{nums}[i] \times \text{nums}[r]\} f ( l , r ) = l < i < r max { f ( l , i ) + f ( i , r ) + nums [ l ] × nums [ i ] × nums [ r ]}
因此可以用 動態規劃(DP) 的方式來解決問題。
方法一:記憶化搜尋(Memoization) / Top-down DP
得到轉移式後,我們可以使用記憶化搜索的方式來解決問題。
比較直覺的方式是用遞迴的方式來解決問題,但是這樣會有大量的重複計算,所以我們需要使用 記憶化搜尋(Memoization) ,將計算過的結果存起來,這樣就可以避免重複計算。在 Python
中可以使用 functools
模組中的 @cache
來進行記憶化搜索,而在 C++
中需要使用一個二維陣列來存儲計算結果。
再來考慮邊界情況,當 r − l ≤ 1 r - l \leq 1 r − l ≤ 1 時,表示只剩下一個或沒有氣球,此時就不需要戳氣球,所以返回 0 0 0 。
寫出遞迴函數後,呼叫 d f s ( 0 , n + 1 ) \rm{dfs}(0, n + 1) dfs ( 0 , n + 1 ) 即可得到答案。
複雜度分析
時間複雜度:O ( n 3 ) \mathcal{O}(n^3) O ( n 3 ) ,總共有 n 2 n^2 n 2 個子問題,每個子問題需要 O ( n ) O(n) O ( n ) 的時間來計算。
空間複雜度:O ( n 2 ) \mathcal{O}(n^2) O ( n 2 ) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def maxCoins (self, nums: List [int ] ) -> int : n = len (nums) nums = [1 ] + nums + [1 ] @cache def dfs (left: int , right: int ) -> int : if right - left <= 1 : return 0 res = 0 for i in range (left + 1 , right): cur = nums[left] * nums[i] * nums[right] cur += dfs(left, i) + dfs(i, right) res = max (res, cur) return res return dfs(0 , n + 1 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution {public : int maxCoins (vector<int >& nums) { int n = nums.size (); vector<int > arr (n + 2 , 1 ) ; for (int i = 1 ; i <= n; i++) arr[i] = nums[i - 1 ]; vector<vector<int >> memo (n + 2 , vector <int >(n + 2 , -1 )); function<int (int , int )> dfs = [&](int left, int right) -> int { if (right - left <= 1 ) return 0 ; if (memo[left][right] != -1 ) return memo[left][right]; int res = 0 ; for (int i = left + 1 ; i < right; i++) { int cur = arr[left] * arr[i] * arr[right]; cur += dfs (left, i) + dfs (i, right); res = max (res, cur); } return memo[left][right] = res; }; return dfs (0 , n + 1 ); } };
方法二:填表法(Tabulation) / Bottom-up DP
類似的,我們也可以使用 填表法(Tabulation) 的方式來解決問題,使用一個二維陣列 d p dp d p 來存儲計算結果,其中 d p [ l ] [ r ] dp[l][r] d p [ l ] [ r ] 表示戳破 [ l , r ] [l, r] [ l , r ] 中的氣球所能獲得的最多硬幣數量。
再來考慮如何開始填表,由於 d p [ l ] [ r ] dp[l][r] d p [ l ] [ r ] 依賴於 d p [ l ] [ i ] dp[l][i] d p [ l ] [ i ] 和 d p [ i ] [ r ] dp[i][r] d p [ i ] [ r ] ,所以我們可以從長度為 3 3 3 的子問題開始填表,然後逐步增加子問題的長度,直到填滿整個表。
對於每個長度 l n ln l n 的子問題,可以枚舉可能的左端點 l l l ,然後計算出右端點 r r r ,再枚舉可能的中間點 m m m ,透過這些中間點 m m m 來計算出 d p [ l ] [ r ] dp[l][r] d p [ l ] [ r ] 。
完成填表後,答案就是 d p [ 0 ] [ n + 1 ] dp[0][n + 1] d p [ 0 ] [ n + 1 ] 。
複雜度分析
時間複雜度:O ( n 3 ) \mathcal{O}(n^3) O ( n 3 ) ,總共有 n 2 n^2 n 2 個子問題,每個子問題需要 O ( n ) O(n) O ( n ) 的時間來計算。
空間複雜度:O ( n 2 ) \mathcal{O}(n^2) O ( n 2 ) 。
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def maxCoins (self, nums: List [int ] ) -> int : n = len (nums) nums = [1 ] + nums + [1 ] dp = [[0 ] * (n + 2 ) for _ in range (n + 2 )] for ln in range (3 , n + 3 ): for l in range (n + 3 - ln): r = l + ln - 1 for m in range (l + 1 , r): dp[l][r] = max ( dp[l][r], dp[l][m] + dp[m][r] + nums[l] * nums[m] * nums[r]) return dp[0 ][n + 1 ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution {public : int maxCoins (vector<int >& nums) { int n = nums.size (); vector<int > arr (n + 2 , 1 ) ; for (int i = 1 ; i <= n; i++) arr[i] = nums[i - 1 ]; vector<vector<int >> dp (n + 2 , vector <int >(n + 2 , 0 )); for (int ln = 3 ; ln <= n + 2 ; ln++) { for (int left = 0 ; left <= n + 2 - ln; left++) { int right = left + ln - 1 ; for (int i = left + 1 ; i < right; i++) { int cur = dp[left][i] + dp[i][right] + arr[left] * arr[i] * arr[right]; dp[left][right] = max (dp[left][right], cur); } } } return dp[0 ][n + 1 ]; } };
寫在最後
Cover photo is generated by @たろたろ , thanks for their work!
事實上,這題也可以視為是 Matrix Chain Multiplication 的變形題,只需要在最前面和最後面加上一個 1 1 1 ,並且從取 min \min min 改為取 max \max max 即可。