算法题之戳气球

戳气球

有 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

提示:

  • n == nums.length
  • 1 <= n <= 300
  • 0 <= nums[i] <= 100

解题思路

这道题,第一眼看上去没有什么思路,一般来说,求最大值和最小值,都可以用动态规划来处理。

首先我们从条件分析,戳破第i个气球,获取的硬币数是i - 1、i和i + 1三者气球数字的乘积。如果超出了数组的边界,就可以假设为数字为1。所以我们可以在气球左右两边分别增加一个数字为1的气球。代码片段如下:

int n = nums.length;
int[] vals = new int[n+2];

vals[0] = vals[n+1] = 1;
for(int i = 1; i < n + 1; i++){
    vals[i] = nums[i-1];
}

我们需要求出下标为1到n的范围内,硬币的最大数量,因为不包含下标0和n + 1,所以我们假设dp[ i ][ j ]代表的是在开区间( i, j )之间硬币的最大数量。

接下来,我们肯定是要开始遍历区间的,怎么遍历呢?我们先确定下标 i 和 j ,再定义一个变量k,使得i < k < j

当i、 k、j 相邻时

戳掉下标为k的气球可以获得的硬币数是:vals[ i ] * vals[ k ] * vals[ j ]

戳气球有哪几种方法呢?

是不是有以下6种情况:

  1. 按照i、k、j的顺序戳破,得到的硬币数为:vals[ i ] * vals[ k ] + vals[ k ] * vals[ j ] + vals[ j ]
  2. 按照i、j、k的顺序戳破,得到的硬币数为:vals[ i ] * vals[ k ] + vals[ k ] * vals[ j ] + vals[ k ]
  3. 按照k、i、j的顺序戳破,得到的硬币数为:vals[ i ] * vals[ k ] * vals[ j ] + vals[ i ] * vals[ j ] + vals[ j ]
  4. 按照k、j、i的顺序戳破,得到的硬币数为:vals[ i ] * vals[ k ] * vals[ j ] + vals[ i ] * vals[ j ] + vals[ i ]
  5. 按照j、i、k的顺序戳破,得到的硬币数为:vals[ k ] * vals[ j ] + vals[ i ] * vals[ k ] + vals[ k ]
  6. 按照j、k、i的顺序戳破,得到的硬币数为:vals[ k ] * vals[ j ] + vals[ i ] * vals[ k ] + vals[ i ]

其实答案不是有这6种情况,看我们上面的定义,在开区间( i, j ),也就是说i、j是边界,所以是不能戳破的,或者说戳破边界能得到的硬币数量是0。所以其实dp[ i ][ j ] = vals[ i ] * vals[ k ] * vals[ j ]

这里我们再引申一下,如果i、j是相邻的,那么dp[ i ][ j ] = 0,因为i、j之间没有可以戳破的气球了;我们后面定义二维数组存放dp[ i ][ j ],正好可以把每个值都初始化为0。

当i、k、k2、j相邻时

  • 情况1.假设我们先戳破k2,那么

dp[ i ][ j ]

= vals[ k ] * vals[ k2 ] * vals[ j ] + vals[ i ] * vals[ k ] * vals[ j ]

= dp[ k ][ j ] + vals[ i ] * vals[ k ] * vals[ j ]

= 0 + dp[ k ][ j ] + vals[ i ] * vals[ k ] * vals[ j ]

= dp[ i ][ k ] + dp[ k ][ j ] + vals[ i ] * vals[ k ] * vals[ j ]

  • 情况2.如果我们先戳破k,那么

dp[ i ][ j ]

= vals[ i ] * vals[ k ] * vals[ k2 ] + vals[ i ] * vals[ k2 ] * vals[ j ]

= dp[ i ][ k2 ] + vals[ i ] * vals[ k2 ] * vals[ j ]

  • 情况3.如果我们把k2和k的位置对换,即i、k2、k、j相邻,先戳破k2,那么

dp[ i ][ j ]

= vals[ i ] * vals[ k2 ] * vals[ k ] + vals[ i ] * vals[ k ] * vals[ j ]

= dp[ i ][ k ] + vals[ i ] * vals[ k ] * vals[ j ]

= dp[ i ][ k ] + 0 + vals[ i ] * vals[ k ] * vals[ j ]

= dp[ i ][ k ] + dp[ k ][ j ] + vals[ i ] * vals[ k ] * vals[ j ]

其实可以发现,dp[ i ][ j ] = dp[ i ][ k ] + dp[ k ][ j ] + vals[ i ] * vals[ k ] * vals[ j ]

在遍历的过程中,情况1的值我们会先存放在dp[ i ][ j ]里,然后和情况3的值比较大小,所以:

dp[ i ][ j ] = max(dp[ i ][ j ], dp[ i ][ k ] + dp[ k ][ j ] + vals[ i ] * vals[ k ] * vals[ j ])

根据上面的推断,就可以得到状态转移方程:

i<j-1时, dp[i]][j] = max_{k=i+1}^{j-1} dp[i][k] + dp[k][j] + vals[i] * vals[k] * vals[j]

i\geq j-1时,dp[i][j]=0

最后,我们注意一下动态规划的次序,具体代码如下:

class Solution {
    public int maxCoins(int[] nums) {
        // 动态规划
        int n = nums.length;
        int[] vals = new int[n+2];
        // 数组两侧增加边界
        vals[0] = vals[n+1] = 1;
        for(int i = 1; i < n + 1; i++){
            vals[i] = nums[i-1];
        }
        
        int[][] dp = new int[n+2][n+2];

        for(int i = n - 1; i >= 0; i--){
            for(int j = i + 2; j <= n + 1; j++){
                for(int k = i + 1; k < j; k++){
                    dp[i][j] = Math.max(dp[i][j], dp[i][k] + dp[k][j] + vals[i] * vals[k] * vals[j]);
                }
            }
        }

        return dp[0][n+1];
    }
}

复杂度分析

  • 时间复杂度:O(n^{3}),有三层遍历,每层都是线性的时间复杂度。
  • 空间复杂度:O(n^{2}),额外定义了一个二维数组。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值