Leetcode算法学习日志-321 Burst Balloons

来源:互联网 发布:云计算板块股票 编辑:程序博客网 时间:2024/06/11 11:24

Leetcode 321 Burst Balloons

题目原文

Given n balloons, indexed from 0 to n-1. Each balloon is painted with a number on it represented by arraynums. You are asked to burst all the balloons. If the you burst ballooni you will get nums[left] * nums[i] * nums[right] coins. Hereleft and right are adjacent indices of i. After the burst, theleft and right then becomes adjacent.

Find the maximum coins you can collect by bursting the balloons wisely.

Note:
(1) You may imagine nums[-1] = nums[n] = 1. They are not real therefore you can not burst them.
(2) 0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100

Example:

Given [3, 1, 5, 8]

Return 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

题意分析

这个题作为一道Hard题实至名归,如果思考稍微不深入,解法的算法复杂度就会很高,造成TLE(Time Limit Exceeded)。题目中戳破一个气球后之前两旁的气球就会相邻,所以我首先想到能不能将剩下的所有气球组成的数组作为原问题的子问题进行求解,这种方法对每一个问题都要遍历它的所有元素,所以复杂度为O(n!),即使加入动态规划的思想,将计算过的数组的最大Coins数记录下来,复杂度依旧很高,这是由于此种大问题化小问题的方法得到的小问题不是大问题的一部分,之前的Add parenthese题目中,遍历每个运算符,可以将问题分为左右两部分,而这里不是,这就造成子问题和原问题不是部分和整体的关系。

更主要的是,如果要对这一道题采用动态规划的方法,一个重要的思想就是逆向思维(Add parenthese即是如此),我们可以遍历最后一个被戳破的气球,由于最后一个气球贡献的Coins和前后的子问题无关,只和这个大问题前后元素有关,所以原问题就被分成了两个互不相关的子问题,且这些子问题在原问题中多次出现,如将它们存储,能减少算法时间。

由题中条件还可以看到,最后一个气球和1乘,所以问了保持算法统一性,减少条件判断,可在数组前后加入两个1.

解法分析

本题主要方法还是递归思想和动态规划的结合,主要有自底向上的动态规划和自顶向下的动态规划两种,下面分别分析:

自顶向下

动态规划方法时常采用逆向思维,去考虑最后一个被戳破的气球,则总的Coins就是最后一个气球的值乘上两边的1,加上两侧子问题的最优解,遍历原问题的每一个元素,可以得到最优解。过程中将求得的子问题最优解存储在关联容器中或者二维数组中,由于关联容器涉及到hash表,其读取速度比数组或vector要慢。

class Solution {public:int maxCoins(vector<int>& nums) {if (nums.size() == 0)return 0;unordered_map<int, int> cache;return dfs(nums, cache, 0, nums.size() - 1);}int dfs(vector<int>& nums, unordered_map<int,int>& cache, int i, int j){int idx = i * nums.size() + j;auto it = cache.find(idx);if (it != cache.end()) return it->second;int res = 0;for (int last = i; last <= j; ++last) {int left = last - 1 < i ? 0 : dfs(nums, cache, i, last-1);int right = last + 1 > j ? 0 : dfs(nums, cache, last+1, j);int mid = (i - 1 < 0 ? 1 : nums[i - 1]) * nums[last] * (j + 1 >= nums.size() ? 1 : nums[j + 1]);res = max(res, left + right + mid);}cache[idx] = res;return res;}};
这种自顶向下的方法有两种情况,第一种是钢条切割问题,遍历后只需要管右侧子问题,对于规模问[1,N]的问题,每个问题只需要求解一次,所以算法复杂度为一个等差数列,结果为O(n^2)。第二种情况为这道题和Add Parenthese的情况,遍历后得到左右两个子问题,T(N)=T(1)+T(N-1)+...T(N-1)+T(1),在Add Parenthese中已分析T(N)=O(3^N),这是上界,由于很多子问题不用多次求解,复杂度会更低。

代码中需要注意的C++知识点如下:

  • 无序关联容器的关键字有限制,一是需要定义==,二是需要有hash模板,所以内置类、String和智能指针可以作为key,而像vector或是一些自定义类,如要作为关键字,需要提供函数代替==和hash计算函数,具体见《C++ Primer》无序关联容器。本题中将子问题的开始和结尾下标通过乘上问题长度得到一个和left、right对应的int型数,作为此子问题序列的关键字,避免了将left和right合成一个String的过程,所以转化关键字类型是很必要的。
  • String、vector等顺序容器,不能在未初始化的时候给相应下标的元素赋值,因为存储空间是未定义的,想完成这种操作,只能给容器初始化为所需大小并付初值。上述代码中,如果以left和right作为二维数组的下标,将相应子问题的解存入数组或vector,则读取速度比关联容器要快。

自底向上

自底向上的方法就是在求解任何一个问题时,它所要用到的子问题都已经被求解,所以需要按照问题的规模来遍历,将所有问题由小到大求解,最终求解的是规模最大的原问题,这种方法一般是多个for循环的嵌套。C++代码如下:

class Solution {public:    int maxCoins(vector<int>& nums) {        int n=nums.size();        int start,end,last,len;        int maxC,temp;        nums.push_back(1);        nums.insert(nums.begin(),1);        vector<vector<int> > dp(nums.size(),vector<int>(nums.size(),0));        for(len=1;len<=n;len++){            for(start=1;start<=n-len+1;start++){                end=start+len-1;                maxC=0;                for(last=start;last<=end;last++){                    temp=nums[start-1]*nums[last]*nums[end+1]+dp[start][last-1]+dp[last+1][end];                    maxC=(maxC<temp)?temp:maxC;                }                dp[start][end]=maxC;            }        }        return dp[1][n];      //也包含了输入为空的情况      }};
自底向上方法的运算复杂度容易判断,直接根据for循环层数即可判断,本题三重for循环,复杂度上界O(n^3),由于采用了二维vector存储,运算速度相较于采用关联容器的方法提高了。

值得注意的C++知识点:

  • 想给相应下标的顺序容器赋值,需要保证容器已经初始化,本题的二维vector采用如下方式初始化:

vector<vector<int> > dp(nums.size(),vector<int>(nums.size(),0));

第一个参数是容器大小,第二个参数是每一个元素的值。


原创粉丝点击