最近在学习动态规划,因为之前初步看了CLRS上面的动态规划部分,觉得已经有了一定的了解,于是想去leetcode上面刷一下动归部分的题目。说实话,不简单。主要还是自己对动态规划没有理解好。按accept从高到底做,做到burst balloons的时候就卡住了,还是看了discussion才恍然大悟,毕竟太弱。以下是对discussion里面的一篇高赞讲解的粗糙翻译,也是对自己思路的一个梳理。原文可以参考:点击打开链接
首先看一下题目描述:
Given n
balloons, indexed from 0
to n-1
. Each balloon is painted with a number on it represented by array nums
. You are asked to burst all the balloons. If the you burst balloon i
you will get nums[left] * nums[i] * nums[right]
coins. Here left
and right
are adjacent indices of i
. After the burst, the left
and right
then becomes adjacent.
Find the maximum coins you can collect by bursting the balloons wisely.
最初的想法:
看到这个题目,很显然,第一反应不是用动态规划解决,而是回溯:
假设现在有n个气球,所以按照题意,若每踩一个气球定义为一个step,则需要n个step才能完成游戏。当进行到第i个step的时候(i<n),还剩下(n-i)个气球,也就是还需要(n-i)step才能完成游戏。用枚举,第一次踩气球,有n种踩法,第2次踩气球有(n-1)中踩法,所以整个游戏有(n!)种完成途径,每个完成途径都可以计算出相应的获得的coin,然后比较一下,取出最大的即可。但是这个算法的复杂度为O(n!),无法接受。所以下面我们逐步优化。
通过观察可以知道。定义:现存的气球集合N,被踩的气球集合M。则 maxcoin(N)和 M 是无关的。也就是已经被踩的气球不会影响到现存的气球的maxcoin计算(这里其实可以看到此问题符合动态规划里面的无后效性,具体可以参考知乎里面一个很好的回答:什么是动态规划?动态规划的意义是什么?)。既然先被踩的气球不会影响后被踩的气球的maxcoin,那我们可以选择先找出被踩两个气球时的maxcoin,被踩三个气球时的maxcoin,......,被踩n个气球时的maxcoin,显然这是一个重叠子问题,并且以上描述显然是一个DP的bottom up思路。但是,计算被踩k个气球时的maxcoin,需要枚举C(n,k)种情况,并进行比较,这导致子问题过多,也是就是每个递归节点有过多的子节点,增加了计算复杂度,虽然比原始的O(n!)要好一点,但并不优于O(2^n),我们需要寻找具有二项式时间的算法。
更好的想法:
根据前面的分析,该问题可以分解为多个子问题,并逐一解决。于是,我们可以尝试是否可以用分治方法来解决呢?
这里需要明确可以用分治方法解决的问题以及可以用DP解决的问题的异同:
分治和DP都需要将原问题分解成小问题,然后逐一解决;不过分治方法的每个小问题都是不相关的,而DP的子问题则是重叠的(overlapping)。可以参见wiki百科上面的解释:dynamic programming。
但是通过前面的分析也知道,之前描述的子问题都是重叠的 (比如你在计算踩K个气球时的maxcoin,肯定会涉及到踩K-1个气球时的结果,这也是可以用bottom up 的意义),因此根本不能用分治方法来求解。自然的一个想法是,我们可不可以先把整体分割,再分别在被分割的各个子整体中用bottom up。这显然是可行的。不过问题在于怎么分割整体,因为整体的分割需要保证各个整体在后面的计算中要保持相互独立性。比如对于[a1,a2,a3,a4,a5,a6,......,an],将分割成两个子整体,分割点为k,则得到 N1 = [a1,a2,a3,....,a(k-1)], N2 = [a(k+1),a(k+2),.....,an]。这里分割点k的意义是踩破了第k个气球。于是把整体分成了两部分,问题在于,根据计算规则,k气球破了之后,a(k-1)和a(k+1)会变成相邻的,如果此时踩a(k-1)或者a(k+1),则都会收到另一个子整体的影响,这样的话,两个子问题就不独立,也就不能用分治了。所以关键的问题在于确定k。
可以发现:
N1和N2相互独立 <=> k点为对于整体N的游戏时,最后一个被踩破的气球。
也就是k点被踩破之前,N1和N2重点的气球都不会相互影响。于是我们就成功构造了子问题。因此分治加dp就可以对问题进行求解了。
写一下状态传递方程:
dp[left][right] = max{dp[left][right] , nums[left] * nums[i] * nums[right] + nums[left] * nums[i] + nums[i] * nums[right]};
其中 left<i<right , dp[left][right]即为当前子问题:第left和第right之间位置的气球的maxcoin。
下面贴一下代码吧
class Solution {
public:
int maxCoins(vector<int>& nums) {
int arr[nums.size()+2];
for(int i=1;i<nums.size()+1;++i)arr[i] = nums[i-1];
arr[0] = arr[nums.size()+1] = 1;
int dp[nums.size()+2][nums.size()+2]={};
int n = nums.size()+2;
for(int k=2;k<n;++k)
{
for(int left = 0;left<n-k;++left){
int right = left + k;
for(int i=left+1;i< right; ++i)
{
dp[left][right] = max(dp[left][right],arr[left]*arr[i]*arr[right] + dp[left][i] + dp[i][right]);
}
}
}
return dp[0][n-1];
}
};
通过这个问题对DP有了更深一点的理解,不过前路漫漫,继续努力吧!
最后谢谢原作者!