美文网首页
力扣(LeetCode) - 312 戳气球

力扣(LeetCode) - 312 戳气球

作者: 小怪兽大作战 | 来源:发表于2019-11-01 11:09 被阅读0次

    本题用记忆化搜索或者动态规划

    零、闲聊

    找完工作有一个多月了,一直在咸鱼。11月了,不能再继续这样了。编程题还要继续刷,感觉一段时间不写,思维确实没有之前灵活了。所以以后还是不断刷题,不断学习。

    一、题目

    有 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 = 315 + 358 + 138 + 181 = 167

    来源:力扣(LeetCode)
    链接:https://leetcode-cn.com/problems/burst-balloons
    著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

    二、分析

    2.1 题目分析

    这道题是很典型的动态规划题。对于这类题,我也是一直没有太多思路。看了一下大神的讲解,感觉明白了一些。
    那这篇博客中我们就从最基础的深度优先遍历,到记忆化搜索,最后到动态规划。三种方法来解决这道题。

    2.2 深度优先搜索

    题目中气球顺序放置,每次扎破一个气球。那么我们在程序中可以使用链表表示气球,每次扎破一个气球,就从链表中删除一个结点。使用深度优先搜索+回溯遍历所有可能的情况。这样肯定可以找到最优解,但是时间复杂度是n!,数据量大的时候肯定会超时的。
    代码如下

        //方法一:深度优先搜索,使用链表存储气球,然后
        public int maxCoins(int[] nums) {
            if (nums == null || nums.length == 0) {
                return 0;
            }
            List<Integer> src = new LinkedList<Integer>(); //链表保存气球
            src.add(1);
            for (int i = 0; i < nums.length; i++) {
                src.add(nums[i]);
            }
            src.add(1);
            return loop(src);
        }
        private int loop(List<Integer> src) {
            if (src.size() <= 2) {  //递归的终止条件
                return 0;
            }
            int res = 0;
            for (int i = 1; i < src.size() - 1; i++) {  //遍历当前每一个结点
                int cur = src.get(i);
                int temp = src.get(i - 1) * cur * src.get(i + 1);  //计算戳爆该结点得到的硬币数量
                src.remove(i);  //删除该结点
                res = Math.max(res, temp + loop(src));  //递归剩下的气球,求出剩下的气球能得到的最大硬币数量
                src.add(i, cur);  //回溯,添加上该结点
            }
            return res;
        }
    

    深度优先搜索可以找到最优解,但是时间复杂度是n!,是因为重复计算了某些子问题。比如,对于[3, 1, 5, 8]这个气球序列,如果我们用深度优先搜索来解,将[5, 8]这个子问题重复计算了两次。


    重复计算了[5, 8]子问题两次

    2.3 记忆化搜索

    简单的深度优先搜索重复计算了子问题,那么我们可以使用数组将子问题的结果保存下来。

    但是在划分子问题的时候有个小技巧
    比如对于[3, 1, 5, 8],可以先戳爆[1],然后剩下[3], [5, 8]两个子问题。但是这有问题的,[3]和[5, 8]不是两个独立的子问题,他们相互关联的,因为戳爆一个气球,剩下的气球又连在一起了,3右边的气球应该是5。
    这时候我们反向思维,对于[3, 1, 5, 8],我们可以最后戳爆[1],这样[3] 和 [5, 8]两个子问题之间就没有关联了。这里比较饶,大家可以看一下这个视频,讲的很好。
    这时候我们可以写出状态转移方程

    for (int k = start; k <= end; k++) {
        res = max(res,  helper(start, k - 1) + src[start - 1] * src[k] * src[end + 1] + helper(k + 1, end)); 
    }
    

    并且把每一个子问题的解都保存到dp中。
    下面看代码

    //方法二 记忆化搜索
        List<Integer> src = null;  //保存气球的数组
        int[][] dp = null;  //dp数组
        int len = 0;
        public int maxCoins(int[] nums) {
            if (nums == null || nums.length == 0) {
                return 0;
            }
            len = nums.length;
            dp = new int[len + 2][len + 2];
            src = new ArrayList<Integer>();
            src.add(1);
            for (int i : nums) {
                src.add(i);
            }
            src.add(1);
            return loop(1, len);
        }
        
        private int loop(int start, int end) {
            if (start > end) {
                return 0;
            }
            if (dp[start][end] > 0) {  //如果有缓存的解,直接返回
                return dp[start][end];
            }
            int res = 0;
            for (int k = start; k <= end; k++) {  //搜索最优解
                //由于我们假设第k个气球是最后戳破的,所有戳破他的时候他的左边和右边的气球分别是start - 1和end + 1
                res = Math.max(res, src.get(start - 1) * src.get(k) * src.get(end + 1) + loop(start, k - 1) + loop(k + 1, end));
            }
            dp[start][end] = res;  //将结果保存到数组中
            return res;
        }
    

    2.4 动态规划

    记忆化搜索将子问题的解缓存到数组中,当递归到相同的子问题时,从缓存中读取,相当于进行深度优先搜索时进行减枝。还是自顶向下的顺序进行搜索,只不过递归到某些已经计算过的子问题时不用重复计算了。
    我们可以自底向上进行计算,先把最基础的子问题计算完成,然后逐层网上计算,最后计算出来结果。
    比如我们先计算长度为1的气球序列的解,然后计算长度为2的气球序列的解,逐层往上。
    代码如下

        //动态规划
        public int maxCoins(int[] nums) {
            if (nums == null || nums.length == 0) {
                return 0;
            }
            int n = nums.length;
            List<Integer> list = new ArrayList<Integer>();
            list.add(1);
            for (int i : nums) {
                list.add(i);
            }
            list.add(1);
            int dp[][] = new int[n + 2][n + 2];
            for (int len = 1; len <= n; len++) {  //逐渐增大计算的粒度
                for (int i = 1; i <= n - len + 1; i++) {  //计算所有该粒度的子问题
                    int j = i + len - 1;
                    for (int k = i; k <= j; k++) {  //计算子问题
                        dp[i][j] = Math.max(dp[i][j], dp[i][k - 1] + dp[k + 1][j] + list.get(i - 1) * list.get(k) * list.get(j + 1));
                    }
                }
            }
            return dp[1][n];
        }
    

    三、参考

    https://www.bilibili.com/video/av45180542

    相关文章

      网友评论

          本文标题:力扣(LeetCode) - 312 戳气球

          本文链接:https://www.haomeiwen.com/subject/wxqfbctx.html