原題鏈接:https://leetcode.com/problems/burst-balloons/
題目的大意是:給你一串氣球,每個氣球上都標了一個數字,這些數字用一個數組nums來表示。如果你扎破第i個氣球,你就可以獲得 nums[left] * nums[i] * nums[right] 個硬幣,其中left和right是與第i個氣球相鄰的兩個氣球,當i被扎破后,left和right就變成直接相鄰了。找出一個扎破所有氣球的順序,使得最后獲得的硬幣數量的總和最大。
解決這道題的思路是動態規划,這道題和經典的矩陣連乘問題很相似,我的算法分析也借鑒了矩陣連乘的求解思路。首先,我們考慮將問題分解成一個或多個子問題,假設第i個氣球是第一個扎破的氣球,我們可以得到 nums[i - 1] * nums[i] * nums[i + 1] + maxCoins(int[] newNums) 個硬幣,其中newNums表示的是刪掉 nums[i] 后由數組nums中剩余元素構成的新數組。如果以這個順序來分解問題,我們每次都要重新調整數組的結構,並且在這種分解順序下,原問題和子問題難以用簡潔的標識符來表示。因此,我們換一種方式考慮,如果假設第i個氣球是最后一個扎破的氣球, 剩下的氣球不管以什么樣的順序扎破,位於第i個氣球的左邊的所有氣球,它們的“right”值只可能小於等於i,而位於第i個氣球右邊的所有氣球,它們的“left”值只可能大於等於i。因此,如果第i個氣球最后扎破,那么它左右兩邊的氣球的扎破順序可以分開來求(按任何順序扎破左半邊的所有氣球不會影響右半邊的最大值,右半邊同理),並且在分解問題的過程中我們不需要調整數組nums的結構。所以,以 maxCoins[0][n - 1]表示原問題的解,則可以做如下分解:
maxCoins[0][n - 1] = maxCoins[0][i - 1] + maxCoins[i + 1][n - 1] + nums[left] * nums[i] * nums[right]
接下來需要考慮 left 和 right 的取值以及邊界問題。對於最后一個扎破的氣球i,得到的硬幣數為 nums[-1] * nums[i] * nums[n],其中 nums[-1] = nums[n] = 1。為了方便計算,我們可以將原數組加上首尾兩個元素,即 nums[-1] 和 nums[n],用一個長度為 n + 2 新的數組,tnums[],來表示。為了方便計算,我們用 maxCoins[low][high] 表示在數組 tnums 中下標范圍為 (low,high),或者說是 [low + 1,high - 1] 的氣球所能獲得的最大硬幣個數,注意這是個開區間,所以原問題的解即 maxCoins[0][tnums.length - 1]。接下來要確定 left 和 right 的取值,對於 maxCoins[low][high],當數組中只剩下最后一個氣球k時, 根據題目中給出的例子,扎破這個氣球得到的硬幣個數是: tnums[low] * tnums[k] * tnums[high] 。寫出狀態轉移方程如下:
maxCoins[low][high] = max{maxCoins[low][k] + maxCoins[k][high] + tnums[low] * tnums[k] * tnums[high] , low < k <high && (high - low) > 2}
or = tnums[low] * tnums[k] * tnums[high] , (high - low) == 2
根據狀態轉移方程,我們可以推算出計算子問題的順序,由 low < k && high > k 可得,maxCoins矩陣的計算順序為大的行下標,小的列下標開始計算,又要保證 low < high,所以計算順序為一層一層的反對角線,這樣可以保證每個問題在求算時需要的子問題已經求算完畢。求算順序由下圖所示:
下面是我用Java寫的代碼:
1 import java.util.Stack; 2 3 public class LeetCode312 { 4 public int maxCoins(int[] nums) { 5 int[] tnums = new int[nums.length + 2]; 6 int n = 1; 7 for (int num : nums) { 8 tnums[n] = num; 9 n++; 10 } 11 tnums[0] = tnums[n++] = 1; 12 13 int[][] coins = new int[n][n]; 14 int[][] burstorders = new int[n][n]; 15 16 int low, high, k; 17 for (low = n - 1; low >= 0; low--) { 18 for (high = low + 2; high < n; high++) { 19 if ((high - low) == 2) { 20 coins[low][high] = tnums[low] * tnums[(low + high) / 2] * tnums[high]; 21 burstorders[low][high] = (low + high) / 2; 22 System.out.println("calculating value of coin[" + low + "][" + high + "]=" + coins[low][high]); 23 } else { 24 for (k = low + 1; k <= high - 1; k++) { 25 int tcoins = coins[low][k] + coins[k][high] + tnums[low] * tnums[k] * tnums[high]; 26 //coins[low][high] = coins[low][high] > tcoins ? coins[low][high] : tcoins; 27 if(coins[low][high] < tcoins){ 28 coins[low][high] = tcoins; 29 burstorders[low][high] = k;//記錄氣球爆破順序 30 } 31 } 32 } 33 } 34 } 35 36 System.out.println("The bursting order is:"); 37 printOrders(burstorders,0,n - 1); 38 39 return coins[0][n - 1]; 40 } 41 42 public void printOrders(int[][] orders,int low,int high){ 43 /*recursive 44 * the output is reverse order 45 if((high - low) == 2){ 46 System.out.println("[" + low + "][" + high + "]:" + (orders[low][high] - 1) + " "); 47 }else if((high - low) > 2){ 48 int last = orders[low][high]; 49 System.out.println("[" + low + "][" + high + "]:" + (last - 1) + " "); 50 printOrders(orders,low,last); 51 printOrders(orders,last,high); 52 }*/ 53 54 //non-recursive 55 class Pair{ 56 private int x; 57 private int y; 58 59 public Pair(int x,int y){ 60 this.x = x; 61 this.y = y; 62 } 63 } 64 //因為orders矩陣記錄的是最后一個扎破的氣球,所以正好用棧把順序倒過來 65 Stack<Integer> burstballoons = new Stack<>(); 66 Stack<Pair> stk = new Stack<>(); 67 Pair p = null; 68 stk.push(new Pair(low,high)); 69 70 while(!stk.isEmpty()){ 71 p = stk.pop(); 72 if((p.y - p.x) == 2){ 73 burstballoons.push(orders[p.x][p.y] - 1); 74 }else if((p.y - p.x) > 2){ 75 burstballoons.push(orders[p.x][p.y] - 1); 76 stk.push(new Pair(orders[p.x][p.y],p.y)); 77 stk.push(new Pair(p.x,orders[p.x][p.y])); 78 } 79 } 80 81 while(!burstballoons.isEmpty()){ 82 System.out.print(burstballoons.pop() + " "); 83 } 84 } 85 }
代碼中還加入了記錄最后一個扎破的氣球的矩陣,用回溯法查詢這個矩陣即可找到一個求解的扎氣球的順序。求解算法包含三層循環,時間復雜度是O(n^3)。