题目描述:
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
示例 1:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物
提示:
0 < grid.length <= 200
0 < grid[0].length <= 200
题解:
题目难度:中等
解题思路:
最近有同学反应我之前的题解都太简单了,笑话我水平菜,我一想确实是这样。今天开始我们就逐步从简单题过渡到中等题。
本题目是一道典型的动态规划入门题。
关于动态规划题目的解题思路,我们首先要明确“状态”的概念,并且推得“状态转移方程”,从而能够将“小区域的最优状态”一步步地推得“目标区域的最优状态”。
大部分没有接触过动态规划的同学,对于本题的解题思路可能会分为以下几种:
- 暴力求解法:设二维数组的长度为m,高度为n,则从左上角走到右下角的方式有m*(n-1)种,我们遍历这个二维数组的m*(n-1)种走法,求得所有走法中的最大值即可。
- 贪心法:走每一步时,我们都面临着向下走还是向右走的选择。我们不如每一步都贪心,选择这两种选择中礼物值较大的一种。
(也有同学可能会想到DFS来求解,其实深搜和暴力算法本质上是一样的,而且滥用递归会造成非常大的空间开支,爆栈的风险比较大。)
显而易见,这几种方法的局限性都比较大。暴力求解法存在的最大问题在于“重复计算”,即浪费了很多时间在求解已经算过的状态值上(这句不理解可以参考北大郭炜老师的mooc),从而超时。而贪心法显然只考虑到了局部的最优,而忽视了全局最优,俗称“顾首不顾尾”。
那么,动态规划的解法是什么样的呢?让我们从思路一步步做起。
- 确定状态:本题目中,我们可以把“在当前所处的位置所能拥有的最大礼物价值”作为状态值。题目中让我们求的,即是在右下角处的位置所能拥有的最大礼物价值。我们不难求得,在左上角的最大礼物价值就是数组左上角的数据值。(res[0][0] = grid[0][0])而第一行和第一列位置对应的最大礼物价值,也显然是从左上角到它们位置的累加和。
- 确定了状态之后,我们就需要推出状态转移方程来从已知的最大礼物价值推得未知的礼物价值了。我们不妨考虑一下,比如在位置[3,2]的时候,这个位置所对应的最大价值可以怎么表示呢?换句话说,它是由[3,1]走过来的呢,还是由[2,2]走过来的呢?显然,当前位置的最大礼物值对应着两者中的较大者+当前位置的礼物值。
(dp[i+1][j+1] = max(dp[i][j+1],dp[i+1][j]) + grid[i][j])
确定了基本思路之后,我们便能够轻松地写出以下代码。
AC代码:
//本函数用于求解右下角位置对应的最大礼物值。
func maxValue(grid [][]int) int {
dp := make([][]int, len(grid)+1)
for i := 0; i < len(dp); i ++{
dp[i] = make([]int, len(grid[0])+1)
} //构造长宽和数组grid一样的二维数组dp,用于存储每个位置对应的最大礼物值。
dp[0][0] = grid[0][0] //显然,在左上角处的最大礼物值,就是左上角的礼物值本身。
for i := 0; i < len(grid); i ++ {
for j:=0; j<len(grid[0]);j++{
//根据状态转移方程,我们不难得出,每一个位置对应的最优状态,都可以由其较大的上一步最优状态值加此位置之值计算出来。
if dp[i][j+1] > dp[i+1][j]{
dp[i+1][j+1] = dp[i][j+1] + grid[i][j]
} else {
dp[i+1][j+1] = dp[i+1][j] + grid[i][j]
}
//这里不得不吐槽Go语言连个max()函数都没有,害得广大群众不得不自己判断大小。
}
}
return dp[len(dp)-1][len(dp[0])-1]//dp数组的右下角对应的最优状态值即为所求之值。
}
关于本题目的单元测试环节,欢迎大家移步:GO语言从入门到入土(3):单元测试
原题目链接:https://leetcode-cn.com/problems/li-wu-de-zui-da-jie-zhi-lcof/submissions/