LeetCode312:戳气球

要求

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

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

思路

只要涉及求最值,没有任何奇技淫巧,一定是穷举所有可能的结果,然后对比得出最值。
穷举主要有两种算法,就是回溯算法和动态规划,前者就是暴力穷举,而后者是根据状态转移方程推导「状态」

方法一:回溯算法
我们其实就是想穷举戳气球的顺序,不同的戳气球顺序可能得到不同的分数,我们需要把所有可能的分数中最高的那个找出来

class Solution {  //O(n!) 超时
    public int maxCoins(int[] nums) {
        int[] state = new int[nums.length];
        for(int i = 0;i<nums.length;i++){
            state[i] = 0;
        }
        tryGetCoins(nums,state,0);
        return max;
    }

    int max = 0;
    void tryGetCoins(int[] nums, int[] state,int current){
        int[] tempState = state.clone();
        int i = 0;
        if(finished(state)){
            max = Math.max(max,current);
            return;
        }
        for(i=0;i<nums.length;i++){
            if(state[i] == 1)
                continue;
            tempState[i] = 1;
            tryGetCoins(nums,tempState,current+getCoinsAt(nums,state,i));
            copyArray(tempState,state);
        }
    }
    int getCoinsAt(int[] nums,int[] state,int i){
        int j = 0,result = nums[i];
        for(j = i-1;j>-1;j--){
            if(state[j] == 0){
                result *= nums[j];
                break;
            }
        }
        for(j = i+1;j<state.length;j++){
            if(state[j] == 0){
                result *= nums[j];
                break;
            }
        }
        return result;
    }
    void copyArray(int[] dest,int[] origin){
        for(int i = 0; i<dest.length;i++){
            dest[i] = origin[i];
        }
    }
    boolean finished(int[] state){
        boolean finished = true;
        for(int i = 0; i < state.length;i++){
            if(state[i] == 0)
                finished = false;
        }
        return  finished;
    }
}

当我们回溯完所有的戳法之后,就能找出最大得分。但是该回溯法的时间复杂度显然为O(n!),而气球个数的取值为[0,500],必然会有超时的问题。

方法二:动态规划
我们每戳破一个气球 nums[i],得到的分数和该气球相邻的气球 nums[i-1] 和 nums[i+1] 是有相关性的。但运用动态规划算法的一个重要条件:子问题必须独立。所以必须巧妙地定义 dp 数组的含义,避免子问题产生相关性,才能推出合理的状态转移方程。
数组两端加入新的虚拟气球,形成一个新的数组 points,现在气球的索引变成了从 1 到 n,points[0] 和 points[n+1] 可以认为是两个「虚拟气球」

状态定义:dp[i][j] = x 表示,戳破气球 i 和气球 j 之间(不包括 i 和 j)的所有气球,获得的最高分数为 x。 base case 就是 dp[i][j] = 0,其中 0 <= i <= n+1, j <= i+1,开区间 (i, j) 中间没有气球可以戳。

// base case 已经都被初始化为 0
int[][] dp = new int[n + 2][n + 2];


推导状态转移方程:
假设 k 是气球 i 和气球 j 之间的所有气球最后被戳破的那一个,需要先把开区间 (i, k) 的气球都戳破,再把开区间 (k, j) 的气球都戳破;最后剩下的气球 k,相邻的就是气球 i 和气球 j,这时候戳破 k 的话得到的分数就是 points[i]*points[k]*points[j]。
dp[i][j] 的值应该为:dp[i][k] + dp[k][j] + points[i]*points[k]*points[j]

在这里插入图片描述

由于是开区间,dp[i][k] 和 dp[k][j] 不会影响气球 k;而戳破气球 k 时,旁边相邻的就是气球 i 和气球 j 了,最后还会剩下气球 i 和气球 j,这也恰好满足了 dp 数组开区间的定义。
对于一组给定的 i 和 j,我们只要穷举 i < k < j 的所有气球 k,选择得分最高的作为 dp[i][j] 的值即可
dp[i][j] 所依赖的状态是 dp[i][k] 和 dp[k][j],那么我们必须保证:在计算 dp[i][j] 时,dp[i][k] 和 dp[k][j] 已经被计算出来了(其中 i < k < j)。

在这里插入图片描述
选择从下往上遍历

public class LeetCode312 {
    public int maxCoins(int[] nums) {
        int n = nums.length;
        //添加两侧的虚拟气球
        int[] points = new int[n + 2];
        points[0] = points[n+1] = 1;
        //遍历数组赋值
        for (int i =1; i <= n ; i++) {
            points[i] = nums[i-1];
        }
        //base case初始化为0
        int[][] dp = new int[n+2][n+2];
        //i从下往上升
        for (int i = n; i >= 0; i--) {
            //j从从左往右
            for (int j = i + 1; j < n + 2; j++) {
                //戳破哪个气球
                for (int k = i + 1; k < j; k++) {
                    //则优做选择
                    dp[i][j] = Math.max(dp[i][j],dp[i][k] + dp[k][j] + points[i]*points[j]*points[k]);
                }
            }
        }
        return dp[0][n+1];
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值