312. 戳气球

159 阅读3分钟

312. 戳气球 - 力扣(Leetcode)

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

现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1 和 i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1或 i + 1 超出了数组的边界,那么就当它是一个数字为 1 的气球。

求所能获得硬币的最大数量。

示例 1:

输入: nums = [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

示例 2:

输入: nums = [1,5]
输出: 10

思路

对于给定的一排气球,我们可以在左右两侧添加1。左右添加1后,设长度是n。那么我们此时求解的范围是(0, n)区间能够获得最大金币数量。注意,是全开区间。此时,求解的问题可设为f(0,n)

未命名文件-导出 (5).png 此时,对于(0, n)区间内,无论怎么戳破气球,肯定会有一个是最后一个戳破的。假如,最后戳破的是2号。那么在戳破2号之前,我们就已经将(0,2)和(2,5)范围内的气球戳破了。还得提醒一下,是全开区间。即,在戳破2号之前,我们已经获得了f(0, 2)f(2, 5)。 所以,戳破2号气球时,获得的值是nums[0]*nums[2]*nums[5]。在(0, n)区间内获得金币值就是f(0, n)=f(0, 2)+f(2, 5) + nums[0]*nums[2]*nums[5]

更通俗一点说,对于(i, j)区间内,无论怎么戳破气球,肯定会有一个最后一个戳破的。假如,最后戳破的是k号。那么在戳破k号之前,我们就已经将(i, k)和(k, j)范围内的气球戳破了。还得提醒一下,是全开区间。即,在戳破k号之前,我们已经获得了f(i, k)f(k, j)。 所以,戳破k号气球时,获得的值是nums[i]*nums[k]*nums[j]。在(i, j)区间内获得金币值就是f(i,j) = f(i, k)+f(k, j) + nums[i]*nums[k]*nums[j]

上述内容我们假定是某一位置做最后一个戳破的位置,求出来该位置获得金币值。

最终的求解方案就是,对于(i, j)全开区间,每一个位置都尝试是最后一个戳破的位置,答案必在其中,取最大值即可。

暴力递归

func maxCoins(nums []int) int {
    nums = append([]int{1}, nums...)
    nums = append(nums, 1)
    n := len(nums)
    return f(nums, 0, n-1)
}


func f(nums []int, i, j int) int {
    // 因为区间范围是(i, j)全开区间,所以i+1==j时,
    // 区间内是没有数的,获取的金币数量为0
    if i+1 == j {
        return 0
    }

    res := 0
    for k := i+1; k<j; k++{
        // 以k位置作为最后戳破的位置,求出来该位置获得金币数量
        v := f(nums, i, k)  + f(nums, k, j)+ nums[i]*nums[k]*nums[j]

        // 取每一个位置可能的最大值
        res = max(res, v)
    }
    
    return res
}

func max(a, b int)int{
    if a > b {
        return a
    }
    return b
}

暴力递归转动态规划

暴力递归中有两个变量,所以我们用一个二维dp。dp的size根据暴力递归中的i,j范围设置。 于是我们现在有了一张二维dp表。

    res := 0
    for k := i+1; k<j; k++{
        // 以k位置作为最后戳破的位置,求出来该位置获得金币数量
        v := f(nums, i, k)  + f(nums, k, j)+ nums[i]*nums[k]*nums[j]

        // 取每一个位置可能的最大值
        res = max(res, v)
    }
    
    return res

根据暴力递归中如上代码可以知道,f(i,j)值的得出依赖于f(i, k)和f(k, j)。我们在dp表中表示出dp[i][j]依赖的位置,可以清楚地看出,dp[i][j]依赖于左边和下边。于是,dp表可以从下到上,从左到右填写。

未命名文件-导出 (6).png

func maxCoins(nums []int) int {
    nums = append([]int{1}, nums...)
    nums = append(nums, 1)
    n := len(nums)
    dp := make([][]int, n)
    for i:=0; i<n; i++{
        dp[i] = make([]int, n)
    }

    for i:=n-1; i>=0; i-- {
        for j:=i; j<n; j++{

            // 接下来6行是暴力递归主体,就是把从f(i, j)拿值变成了从dp[i][j]拿值
            res := 0
            for k := i+1; k<j; k++{
                v := dp[i][k] + nums[i]*nums[k]*nums[j] + dp[k][j]
                res = max(res, v)
            }
            dp[i][j] = res
        }
    }

    return dp[0][n-1]

}


func getValue(dp [][]int,i, j int) int {
    if i+1 == j {
        return 0;
    }
    
    return dp[i][j];
}


func max(a, b int)int{
    if a > b {
        return a
    }
    return b
}