Leetcode 312. 戳气球
题目
首先必须要说明,这个题目的状态转移方程真的比较巧妙,所以说如果你看了题目之后完全没有思路恰恰是正常的。虽然最优答案不容易想出来,但基本的思路分析是我们应该力求做到的。所以本文会先分析一下常规思路,然后再引入动态规划解法。
一、回溯思路
先来顺一下解决这种问题的套路:
我们前文多次强调过,很显然只要涉及求最值,没有任何奇技淫巧,一定是穷举所有可能的结果,然后对比得出最值。
所以说,只要遇到求最值的算法问题,首先要思考的就是:如何穷举出所有可能的结果?
穷举主要有两种算法,就是回溯算法和动态规划,前者就是暴力穷举,而后者是根据状态转移方程推导「状态」。
如何将我们的扎气球问题转化成回溯算法呢?这个应该不难想到的,我们其实就是想穷举戳气球的顺序,不同的戳气球顺序可能得到不同的分数,我们需要把所有可能的分数中最高的那个找出来,对吧。
那么,这不就是一个「全排列」问题嘛,我们前文 回溯算法框架套路详解 中有全排列算法的详解和代码,其实只要稍微改一下逻辑即可,伪码思路如下:
int res = Integer.MIN_VALUE;
/* 输入一组气球,返回戳破它们获得的最大分数 */
int maxCoins(int[] nums) {backtrack(nums, 0); return res;
}
/* 回溯算法的伪码解法 */
void backtrack(int[] nums, int socre) {if (nums 为空) {res = max(res, score);return;}for (int i = 0; i < nums.length; i++) {int point = nums[i-1] * nums[i] * nums[i+1];int temp = nums[i];// 做选择在 nums 中删除元素 nums[i]// 递归回溯backtrack(nums, score + point);// 撤销选择将 temp 还原到 nums[i]}
}
回溯算法就是这么简单粗暴,但是相应的,算法的效率非常低。这个解法等同于全排列,所以时间复杂度是阶乘级别,非常高,题目说了 nums 的大小 n 最多为 500,所以回溯算法肯定是不能通过所有测试用例的。
二、动态规划思路
这个动态规划问题和我们之前的动态规划系列文章相比有什么特别之处?为什么它比较难呢?
原因在于,这个问题中我们每戳破一个气球 nums[i],得到的分数和该气球相邻的气球 nums[i-1] 和 nums[i+1] 是有相关性的。
我们前文动态规划套路框架详解 说过运用动态规划算法的一个重要条件:子问题必须独立。所以对于这个戳气球问题,如果想用动态规划,必须巧妙地定义 dp 数组的含义,避免子问题产生相关性,才能推出合理的状态转移方程。
如何定义 dp 数组呢,这里需要对问题进行一个简单地转化。题目说可以认为 nums[-1] = nums[n] = 1,那么我们先直接把这两个边界加进去,形成一个新的数组 points:
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];
// 最后戳破的气球是哪个?
for (int k = i + 1; k < j; k++) {// 择优做选择,使得 dp[i][j] 最大dp[i][j] = Math.max(dp[i][j], dp[i][k] + dp[k][j] + points[i]*points[j]*points[k]);
}
写出状态转移方程就完成这道题的一大半了,但是还有问题:对于 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] + points[i]*points[j]*points[k]);
return dp[0][n+1];
三、写出代码
对于任一 dp[i][j],我们希望所有 dp[i][k] 和 dp[k][j] 已经被计算,画在图上就是这种情况:
那么,为了达到这个要求,可以有两种遍历方法,要么斜着遍历,要么从下到上从左到右遍历:
斜着遍历有一点难写,所以一般我们就从下往上遍历,下面看完整代码:
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 已经都被初始化为 0int[][] 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];
}
至此,这道题目就完全解决了,十分巧妙,但也不是那么难,对吧?
关键在于 dp 数组的定义,需要避免子问题互相影响,所以我们反向思考,将 dp[i][j] 的定义设为开区间,考虑最后戳破的气球是哪一个,以此构建了状态转移方程。
对于如何穷举「状态」,我们使用了小技巧,通过 base case 和最终状态推导出 i,j 的遍历方向,保证正确的状态转移。
参考资料
https://leetcode.cn/problems/burst-balloons/solution/dong-tai-gui-hua-tao-lu-jie-jue-chuo-qi-qiu-wen-ti/
Leetcode 312. 戳气球相关推荐
- Leetcode.312 戳气球
题目链接 Leetcode.312 戳气球 题目描述 有 n个气球,编号为0到 n - 1,每个气球上都标有一个数字,这些数字存在数组 nums中. 现在要求你戳破所有的气球.戳破第 i 个气球,你可 ...
- LeetCode 312.戳气球
LeetCode 312.戳气球 有 n 个气球,编号为0 到 n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中. 现在要求你戳破所有的气球.戳破第 i 个气球,你可以获得 num ...
- leetcode: 312. 戳气球
312. 戳气球 来源:力扣(LeetCode) 链接: https://leetcode.cn/problems/burst-balloons/ 有 n 个气球,编号为0 到 n - 1,每个气球上 ...
- LeetCode·312.戳气球·动态规划
312.戳气球 题目 示例 思路 首先必须要说明,这个题目的状态转移方程真的比较巧妙,所以说如果你看了题目之后完全没有思路恰恰是正常的.虽然最优答案不容易想出来,但基本的思路分析是我们应该力求做到的. ...
- Java实现 LeetCode 312 戳气球
312. 戳气球 有 n 个气球,编号为0 到 n-1,每个气球上都标有一个数字,这些数字存在数组 nums 中. 现在要求你戳破所有的气球.每当你戳破一个气球 i 时,你可以获得 nums[left ...
- LeetCode 312. 戳气球(DP,难)
1. 题目 有 n 个气球,编号为0 到 n-1,每个气球上都标有一个数字,这些数字存在数组 nums 中. 现在要求你戳破所有的气球.每当你戳破一个气球 i 时,你可以获得 nums[left]∗n ...
- LeetCode 312. 戳气球(Burst Balloons)
题目描述: 有 n 个气球,编号为0 到 n-1,每个气球上都标有一个数字,这些数字存在数组 nums 中. 现在要求你戳破所有的气球.每当你戳破一个气球 i 时,你可以获得 nums[left] * ...
- Leetcode 312. 戳气球(经典区间dp)
为了避免边界问题,我们重新开一个数组,在两个端点加入哨兵1.此时数组下标为0到n+1 状态的定义如下,f[i][j] 表示将区间[i+1,j-1]的气球全部戳爆的最大收益. 我们要求的答案就是f[0] ...
- LeetCode 312. 戳气球(Java)
题目 有 n 个气球,编号为0 到 n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中. 现在要求你戳破所有的气球.戳破第 i 个气球,你可以获得 nums[i - 1] * num ...
最新文章
- js高级程序设计(六)面向对象
- 读博五年,我总结出了7条帮你「少走弯路」的真理
- TCP客户机-服务器
- 爬取最好大学网站大学排名
- 计算机视觉,图像处理 经典代码paper整理(很全很强大!)
- 第四章:滚动堆栈(1)
- 基于检索的自然语言处理模型研究梳理
- MP3播放器横向比较专题之二:闪存式
- c语言初学者程序,C语言初学者必懂的100个范例程序
- 仿网易云音乐小程序-uniapp
- 路由器连接猫服务器未响应,路由器连接猫不能上网怎么办
- 农业知识图谱(Agriculture_KnowledgeGraph)项目环境构建
- c语言的switch中case,c语言switch中case语句
- 第1章 Redis查阅官网和基本配置
- 计算机控制字如何使用,字由怎么用?一款软件管理1594种字体 很多设计大神都在用...
- 带有源代码的2020年20种最佳HTML5游戏模板
- mysql 判断质数_2020-09-20:如何判断一个数是质数?
- 【系统分析师之路】2011年系统分析师下午案例分析真题
- 你的计划为什么执行不下去 怎么破
- puking java_GitHub - pukingli/mpsdk4j: JAVA微信公平台开发SDK,没有复杂的功能,一切源于微信API,愿你会喜欢使用。...