11.动态规划:戳气球

戳气球(LeetCode 312题 难度:困难)

题目中说 nums[0]=nums[n]=1;

动态规划思路:

int n=nums.length;  
  
int point[]=new int[n+2];  
int m=point.length;  
  
point[0]=point[n+1]=1;  
  
for (int i = 0; i < n; i++) {  
    point[i+1]=nums[i];  
}

现在气球的索引变成了从1到n,nums[0]和nums[n+1]看做两个虚拟气球
那么可以改变问题:在一排气球point中,请戳破0到n+1之间所有气球(不包括0和n+1),使得最终只剩下0和气球n+1两个气球,最多能够得到多少分?

dp数组定义:

	dp[i][j]=x表示,戳破气球i和气球j之间(开区间,不包括i和j)的所有气球,
	可以获得最高获得的分数为x,简单来说就是 i j 是开区间
	根据这个定义,题目中要求的结果 就是 dp[0][n+1]或者dp[0][m-1]
	
	根据以往的经验,可以知道,斜着遍历

base case

	dp[i][i]=0;开区间没有气球可以戳破
//所有的数字都已经初始化为0
int dp[][]=new int[m][m]; //m就是n+2

其实气球i和气球j之间的所有气球都有可能是被戳破的那一个,不放假设为k,回顾一下动态规划的套路,这里其实已经找到了状态和选择:i和j就是两个状态,最后戳破的那个气球k就是选择
不是要求戳破气球k嘛,那要把开区间(i,k)的气球都戳破,再把开区间(k,j)的气球也戳破;最后剩下气球k,相邻的气球就是气球i和气球j,这时候戳破k的话得到的分数 point[i] x point[j] x point [k]
那么戳破开区间(i,j)和开区间(k,j)的气球最多可以得到的分数是多少呢?那就是dp[i][k]+dp[k][i]+ point[i] x point[j] x point [k]


那么对于一组给定的i和j,只要穷举 i<k<j 的所有气球k,然后选择得分最高的给dp[i][j]就OK了。

//在i和j这个区间中,最后戳破哪个气球收益最大就给dp[i];
for (int k = i+1; k <j ; k++) {  
	//择优选择
    dp[i][j]=Math.max(dp[i][j],dp[i][k]+dp[k][j]+point[i]*point[k]*point[j]);  
}
  • 对于k的穷举仅仅是在做选择,但是应该如何穷举 状态 i 和 j 呢?
for (int i =....) {  
    for (int j =....) {  
  		//择优选择
        for (int k = i+1; k <j ; k++) {  
            dp[i][j]=Math.max(dp[i][j],dp[i][k]+dp[k][j]+point[i]*point[k]*point[j]);  
        }  
  
    }  
}

写出代码

关于「状态」的穷举,最重要的一点就是:状态转移所依赖的状态必须被提前计算出来

拿这道题举例,dp[i][j]所依赖的状态是dp[i][k]dp[k][j],那么我们必须保证:在计算dp[i][j]时,dp[i][k]dp[k][j]已经被计算出来了(其中i < k < j)。

那么应该如何安排ij的遍历顺序,来提供上述的保证呢?我们前文 动态规划答疑篇 写过处理这种问题的一个鸡贼技巧:根据 base case 和最终状态进行推导

PS:最终状态就是指题目要求的结果,对于这道题目也就是dp[0][n+1]

我们先把 base case 和最终的状态在 DP table 上画出来:

对于任一dp[i][j],我们希望所有dp[i][k]dp[k][j]已经被计算,画在图上就是这种情况:

那么,为了达到这个要求,可以有两种遍历方法,要么斜着遍历,要么从下到上从左到右遍历:

public int maxCoins(int[] nums) {  
    int n=nums.length;  
  
    int point[]=new int[n+2];  
    int m=point.length;  
  
    point[0]=point[n+1]=1;  
  
    for (int i = 0; i < n; i++) {  
        point[i+1]=nums[i];  
    }  
    int dp[][]=new int[m][m];  
    for (int i = m-2; i >=0 ; i--) {  
        for (int j = i+1; j < m ; j++) {  
  			//择优录取
            for (int k = i+1; k <j ; k++) {  
                dp[i][j]=Math.max(dp[i][j],dp[i][k]+dp[k][j]+point[i]*point[k]*point[j]);  
            }  
  
        }  
    }  
    return dp[0][m-1];  
}

总结

至此,这道题目就完全解决了,十分巧妙,但也不是那么难,对吧?

关键在于dp数组的定义,需要避免子问题互相影响,所以我们反向思考,将dp[i][j]的定义设为开区间,考虑最后戳破的气球是哪一个,以此构建了状态转移方程。

对于如何穷举「状态」,我们使用了小技巧,通过 base case 和最终状态推导出i,j的遍历方向,保证正确的状态转移。

posted @ 2021-07-16 12:09  宋佳强  阅读(82)  评论(0编辑  收藏  举报