312.戳气球
有n
个气球,编号为0
到n-1
,每个气球上都标有一个数字,这些数字存在数组nums
中。
现在要求你戳破所有气球。如果你戳破气球i
,就可以获得nums[left]*nums[i]*nums[right]
个硬币。这里的left
和right
代表和i
相邻的两个气球的序号。注意当你戳破了气球i
后,气球left
和气球right
就编程了相邻的气球。
求所能获得的硬币最大数量。
说明:
* 那你可以假设`nums[-1] = nums[n] = 1`,但注意它们不是真实存在的,所以不能被戳破。
* 0<=`n`<=500, 0<=`nums[i]`<=100
示例
输入:[3, 1, 5, 8]
输出: 167
解释: nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167
解题思路
解法一: 回溯法
本题使用回溯法是以一种暴力求解的方式解决此问题,通过遍历所有的可能,找到最大值。下面给出代码:
private int total = 0;
public int maxCoins(int[] nums) {
List<Integer> list = new ArrayList<>();
for (int num : nums) {
list.add(num);
}
helper(list, 0);
return total;
}
public int helper(List<Integer> nums, int coins) {
int curCoins = 0;
if (nums.size() == 0) {
total = Math.max(coins, total);
return;
}
for (int i = 0; i < nums.size(); i++) {
int temp = nums.get(i);
curCoins = nums.get(i) * (i - 1 < 0 ? 1 : nums.get(i - 1)) * (i + 1 == nums.size() ? 1 : nums.get(i + 1));
nums.remove(i);
helper(nums, curCoins + coins);
nums.add(i, temp);
}
}
稍微解释一下上面的代码,为了操作方便 ,我这里选择了用List
代替整型数组。通过递归的方式对所有的可能进行遍历,递归函数的参数nums
和coins
分别代表当前数组以及获得的硬币的数量。递归的出口就是当前数组为空,并且当数组为空时与之前所求得的硬币最大值比较,看当前方法获得的硬币是否为最大值。这里值得注意的是,删除完元素后别忘记了加回去。
回溯法虽然能得到正确的结果,但是不难看出,时间复杂度为O(n!)
。一般来说O(n!)
的时间复杂度在n > 10
的条件下就通不过了,而这里的n
是[0, 500]
,因此该方法并不能通过。
解法二: 动态规划
动态规划的核心是找到状态转移方程,将问题划分为两个最佳子问题。从正向考虑,将第i
个气球看成是当前要戳爆的气球的话,子问题就划分在[0, i - 1]
和[i + 1, n)
这两个区间。但是根据规则,戳爆了第i
个气球之后,第i - 1
和第i + 1
个气球直接相关,这样我们就没法将两个区间看成是独立的子问题。
所以,不妨逆向考虑,将第i
个气球当成是最后被戳爆的气球。那么,[0, i - 1]
和[i + 1, n)
就只和i
相关了,这样就可以将两个区间看成是相互独立的子问题了。
建立dp[i][j]
数组代表从i
到j
所能获得硬币的最大数量,k(i <= k <= j)
代表了当前第k
个气球被戳爆。那么这个状态转移方程就可以写成dp[i][j] = dp[i][k - 1] + dp[k + 1][j] + nums[i - 1] * nums[k] * nums[j + 1]
。下面给出代码:
class Solution {
private int[][] dp;
private List<Integer> list = new ArrayList<>();
public int maxCoins(int[] nums) {
dp = new int[nums.length + 2][nums.length + 2];
for (int num : nums) {
list.add(num);
}
list.add(0, 1);
list.add(nums.length + 1, 1);
helper(list, 1, nums.length);
return dp[1][nums.length];
}
public int helper(List<Integer> nums, int i, int j) {
if (i > j) {
return 0;
}
if (dp[i][j] > 0) {
return dp[i][j];
}
for (int k = i; k <= j; k++) {
int left = helper(nums, i, k - 1);
int right = helper(nums, k + 1, j);
int delta = nums.get(i - 1) * nums.get(k) * nums.get(j + 1);
dp[i][j] = Math.max(dp[i][j], left + right + delta);
}
return dp[i][j];
}
}
注意,这种实现方法的时间复杂度还是O(n!)
,但是不同是的dp[i][j]
数组可以记录从i
到j
所能获得的硬币数量,可以进行剪枝操作,避免了重复计算。
后记
最初这道题我的想法是用贪心来解决,思路是找到一个气球周围两个气球的乘积的最大值,并点爆这个气球(若乘积相同则点爆硬币小的气球)。结果试验后发现这个想法是错的,于是想用动态规划的方法解决,然而并没有找到状态转移方程。最终上述的代码是我看别人的解题思路后实现的,希望以后碰见这类问题可以有一个快速的反应,也希望自己的算法水平能提高,加油。