LeetCode312 戳气球

312.戳气球


n个气球,编号为0n-1,每个气球上都标有一个数字,这些数字存在数组nums中。

现在要求你戳破所有气球。如果你戳破气球i,就可以获得nums[left]*nums[i]*nums[right]个硬币。这里的leftright代表和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代替整型数组。通过递归的方式对所有的可能进行遍历,递归函数的参数numscoins分别代表当前数组以及获得的硬币的数量。递归的出口就是当前数组为空,并且当数组为空时与之前所求得的硬币最大值比较,看当前方法获得的硬币是否为最大值。这里值得注意的是,删除完元素后别忘记了加回去。

回溯法虽然能得到正确的结果,但是不难看出,时间复杂度为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]数组代表从ij所能获得硬币的最大数量,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]数组可以记录从ij所能获得的硬币数量,可以进行剪枝操作,避免了重复计算。

后记

最初这道题我的想法是用贪心来解决,思路是找到一个气球周围两个气球的乘积的最大值,并点爆这个气球(若乘积相同则点爆硬币小的气球)。结果试验后发现这个想法是错的,于是想用动态规划的方法解决,然而并没有找到状态转移方程。最终上述的代码是我看别人的解题思路后实现的,希望以后碰见这类问题可以有一个快速的反应,也希望自己的算法水平能提高,加油。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值