原题链接: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)。