算法总结4 动态规划
- 动态规划的理解
-
- 1.1、基础问题1(线性DP)
- 1.2、基础问题2(线性DP)
- 1.3、背包问题(背包DP)
- 1.4、打家劫舍(线性DP)
- 1.5、股票问题 -(状态DP)
- 1.5、子序列问题(线性DP)
- 1.6、练习
动态规划的理解
动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
动态规划中每一个状态一定是由上一个状态推导出来的。而贪心算法不同,贪心没有状态推导,而是从局部直接选最优的。
动态规划问题,将被拆解为如下五步:
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
注意: 有些题目非常相似,但实际上做法不同,比如1.5.4.2 里的 leetcode的 5. 最长回文子串与牛客网的HJ32 密码截取都是求最长回文的子串,它是字符串,而 1.5.4.3里的 516. 最长回文子序列求的是子序列的长度,它是数字。所以,求长度则使用动态规划,求字符串则使用回溯,贪心等等其他算法,当然使用动态规划也是可以做出来的,但是时间复杂度相对高一些。
1.1、基础问题1(线性DP)
1.1.1、509. 斐波那契数列
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
1一般解法:递归:
class Solution:
def fib(self, n: int) -> int:
if n < 2:
return n
else:
return self.fib(n-1) + self.fib(n-2)
时间复杂度:O(2^n)
空间复杂度:O(n)
2高级解法:动态规划:
- 确定dp数组(dp table)以及下标的含义
dp[i]的含义是:第i个数的斐波那契数值是dp[i] - 确定递推公式
题目已经给我们了,状态转移方程:F(n) = F(n - 1) + F(n - 2),其中 n > 1 - dp数组如何初始化
题目也直接给了我们,F(0) = 0,F(1) = 1 - 确定遍历顺序
从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的 - 举例推导dp数组
按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2],我们来推导一下,当N为10的时候,dp数组应该是如下的数列:
0 1 1 2 3 5 8 13 21 34 55
如果代码写出来,发现结果不对,就把dp数组打印出来看看和我们推导的数列是不是一致的。
综上,代码如下:
class Solution:
def fib(self, n: int) -> int:
if n < 2:
return n
dp = [0]*(n+1)
dp[0] = 0
dp[1] = 1
for i in range(2, n+1):
dp[i] = dp[i-1]+dp[i-2]
return dp[n]
时间复杂度:O(n)
空间复杂度:O(n)
上面我们维护了一个长度为N+1的数组,实际上我们只需要维护两个数值就行,因为我们只需要最后一个数。修改数组为两个数值:
class Solution:
def fib(self, n: int) -> int:
if n < 2:
return n
dp1, p2 = 0, 1
for i in range(2, n+1):
dp1, dp2 = dp2, dp1+dp2
return dp2
时间复杂度:O(n)
空间复杂度:O(1)
1.1.2、70. 爬楼梯
1一般解法:递归(超时):
class Solution:
def climbStairs(self, n: int) -> int:
if n<2:
return n
else:
return self.climbStairs(n-1) + self.climbStairs(n-2)
2高级解法:动态规划:
- 确定dp数组(dp table)以及下标的含义
dp[i]的含义是:上第i个阶梯的步数为dp[i] - 确定递推公式
因为一次走一步或两步,dp[i-1]往上走一个台阶为dp[i],dp[i-2]往上走两个台阶为dp[i],那么dp[i] = dp[i-1]+dp[i-2] - dp数组如何初始化
这里比较有争议的点是,dp[0]是0还是1。按照公式递推,dp[0]应该是1,但是按照实际场景,0阶台阶走的步数应该是0。
这里直接说明,不考虑dp[0]。如果初始化,只初始化dp[1] = 1,dp[2] = 2,然后从i = 3开始递推,这样才符合dp[i]的定义。 - 确定遍历顺序
从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的 - 举例推导dp数组
举例当n为5的时候,dp table(dp数组)应该是这样的:
可以看出这就是上一题的斐波那契数列,唯一的区别是,没有讨论dp[0]应该是什么,因为dp[0]在本题没有意义。
根据规则代码如下:
class Solution:
def climbStairs(self, n: int) -> int:
# n为1 和 2
if n<3:
return n
# 初始化
# 一阶楼梯
dp1 = 1
# 二阶楼梯
dp2 = 2
# 递归公式
for i in range(3, n+1):
dp1, dp2 = dp2, dp1+dp2
return dp2
1.1.3、746. 使用最小花费爬楼梯
高级解法:动态规划:
- 确定dp数组以及下标的含义:
动态规划需要有一个数组来记录状态,这里只用一个一维数组dp[i]就可以了。
dp[i]的定义:到达第i个台阶所花费的最少体力为dp[i]。 - 确定递推公式:
因为一次走一步或者两步,那么可以有两个途径得到dp[i],一个是dp[i-1] 一个是dp[i-2]。
因为是求花费的体力最小,那么一定是选最小的,所以dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i],这个意思是,选取前一步或者两步的最小值,加上走到当前台阶的体力,等于从头开始走到当前的总体力花费。 - dp数组如何初始化
同理,题目中由台阶0或1开始,那么初始化dp[0] 和dp[1]就够了,后面的由公式递推。
dp0, dp1 = cost[0], cost[1]
-
确定遍历顺序
因为是模拟台阶,而且dp[i]又dp[i-1]dp[i-2]推出,所以是从前到后遍历cost数组就可以了。 -
举例推导dp数组
拿示例2:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] ,来模拟一下dp数组的状态变化,如下:
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
n = len(cost)
dp = [0]*(n)
dp[0] = cost[0]
dp[1] = cost[1]
for i in range(2,n):
dp[i] = min(dp[i-1], dp[i-2]) + cost[i]
return min(dp[n-1], dp[n-2])
时间复杂度:O(n)
空间复杂度:O(n)
这种写法或许更好理解,n+1才是顶部
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
n = len(cost)
dp = [0]*(n+1)
for i in range(2, n+1):
dp[i] = min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2])
print(dp)
return dp[-1]
这里同样可以只维持两个值就行了,从而使代码:
时间复杂度:O(n)
空间复杂度:O(1)
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
n = len(cost)
dp = [0, 0]
for i in range(2, n+1):
dp[0], dp[1] = dp[1], min(dp[1]+cost[i-1], dp[0]+cost[i-2])
return dp[-1]
但为了直观,平时还是建议上述第一种写法。
1.2、基础问题2(线性DP)
1.2.1、62. 不同路径
1.一般解法,广度深度优先搜索,递归(超时):
可以转化为求二叉树叶子节点的个数
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
def dfs(i, j, m, n):
# 越界
if i>m or j>n:
return 0
# 找到一种方法
if i==m and j==n:
return 1
# 递归,类似于走楼梯,而这里是往右或者往下走
return dfs(i+1, j, m, n) + dfs(i, j+1, m, n)
return dfs(1, 1, m, n)
2.动态规划:
-
确定dp数组(dp table)以及下标的含义
dp[i][j] :表示从(0, 0)出发,到(i, j) 有dp[i][j]条不同的路径。 -
确定递推公式
要求dp[i][j],得由前面的一个点推导而来,有两个,即上面的点dp[i - 1][j] 和 左边的点dp[i][j - 1]。
于是显而易见,公式为:dp[i][j] = dp[i - 1][j] + dp[i][j - 1],因为dp[i][j]只有这两个方向过来。 -
dp数组的初始化
如何初始化呢,首先dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理。
所以初始化代码为:
for i in range(m):
dp[i][0] = 0
for i in range(n):
dp[0][j] = 0
-
确定遍历顺序
这里要看一下递归公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1],dp[i][j]都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了。
这样就可以保证推导dp[i][j]的时候,dp[i - 1][j] 和 dp[i][j - 1]一定是有数值的。 -
举例推导dp数组
如图所示:
第一次可能写成这样:
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [[0]*n for _ in range(m)]
for i in range(n):
dp[0][i]=1
for j in range(m):
dp[j][0]=1
for i in range(1, m):
for j in range(1, n):
dp[i][j]=dp[i-1][j]+dp[i][j-1]
return dp[-1][-1]
但实际上,在创建dp的时候就可以都初始化为1,因为dp[i][j] = dp[i-1][j] + dp[i][j-1],原dp[i][j]的1值不影响。
综上,代码如下:
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
# 二维数组
dp = [[1]*n for _ in range(m)]
# 数组赋值
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[m-1][n-1]
时间复杂度:O(m × n)
空间复杂度:O(m × n)
同样的,在理解上述方法中表格的值的规律的情况下,可以只用维护一个一维数组,来减少空间复杂度,下一行等于上一行的与前一个点的和。
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [1]*n
for i in range(1, m):
for j in range(1, n):
dp[j] += dp[j-1]
return dp[n-1]
时间复杂度:O(m × n)
空间复杂度:O(n)
一般这第二种同样也可能不便于理解,使用第一种方法即可。
1.2.2、63. 不同路径Ⅱ
动态规划:
动规五部曲:
- 确定dp数组(dp table)以及下标的含义
dp[i][j] :表示从(0, 0)出发,到(i, j) 有dp[i][j]条不同的路径。
这个同前面的题的含义 - 确定递推公式
递推公式和 62.不同路径 一样,为dp[i][j] = dp[i - 1][j] + dp[i][j - 1]。
但这里需要注意一点,因为有了障碍,(i, j)如果就是障碍的话应该就保持初始状态(初始状态为0)。
所以需要加入条件判断:
# 当(i, j)没有障碍的时候,再推导dp[i][j]
if obstacleGrid[i][j] == 0:
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
# 或者
if obstacleGrid[i][j] == 1:
continue
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
- dp数组如何初始化
在 62.不同路径 不同路径中我们给出如下的初始化:
dp = [[0]*n for _ in range(m)]
for i in range(m):
dp[0][i] = 1
for j in range(n):
dp[j][0] = 1
"""
虽然是初始化是:dp = [[1]*n for _ in range(m)],但这是简写,但实际上为上面的过程。
"""
但这道题,如上图,如果(i, 0) 这条边有了障碍之后,障碍之后(包括障碍)都是走不到的位置了,所以障碍之后的dp[i][0]应该还是初始值0。正确的初始化如下:
dp = [[0]*n for _ in range(m)]
for i in range(m):
if obstacleGrid[0][i] == 1:
break
dp[0][i] = 1
for j in range(n):
if obstacleGrid[j][0] == 1:
break
dp[j][0] = 1
注意代码里for循环的终止条件,一旦遇到obstacleGrid[i][0] == 1的情况就停止dp[i][0]的赋值1的操作,dp[0][j]同理。
- 确定遍历顺序
从递归公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1] 中可以看出,一定是从左到右一层一层遍历,这样保证推导dp[i][j]的时候,dp[i - 1][j] 和 dp[i][j - 1]一定是有数值。
for i in range(1, m):
for j in range(1, n):
if obstacleGrid[i][j] == 1:
continue
dp[i][j] = dp[i-1][j] + dp[i][j-1]
- 举例推导dp数组
拿示例1来举例如题:
对应的dp table 如图:
综上所述:
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
length = len(obstacleGrid)
width = len(obstacleGrid[0])
# 如果障碍物在起点或终点,直接返回0
if obstacleGrid[0][0] == 1 or obstacleGrid[length-1][width-1]==1:
return 0
# 创建一个dp table
dp = [[0]*width for _ in range(length)]
# 两个循环,初始化 dp table
for i in range(length):
# 遇到障碍物,后续都为0
if obstacleGrid[i][0] == 1:
break
dp[i][0] = 1
for j in range(width):
# 遇到障碍物,后续都为0
if obstacleGrid[0][j] == 1:
break
dp[0][j] = 1
# 填表
for i in range(1, length):
for j in range(1, width):
if obstacleGrid[i][j] == 1:
continue
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[length-1][width-1]
时间复杂度:O(n × m),n、m 分别为obstacleGrid 长度和宽度
空间复杂度:O(n × m)
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
length = len(obstacleGrid)
width = len(obstacleGrid[0])
# 如果障碍物在起点或终点,直接返回0
if obstacleGrid[0][0] == 1 or obstacleGrid[length-1][width-1]==1:
return 0
# 创建一个dp table
dp = [0]*width
# 一个循环,先从obstacleGrid第一行开始
for i in range(width):
# 遇到障碍物,后续都为0
if obstacleGrid[0][i] == 1:
break
dp[i] = 1
# 填表
# 从第二行开始
for i in range(1, length):
# 这里不能从1开始,因为可能位置0有障碍物,所以从第0列开始
for j in range(width):
# 有障碍物,走法数量直接为0
if obstacleGrid[i][j] == 1:
dp[j] = 0
# 这里不能直接else,否则会用下面公式累加而改变第一列的值
elif j!=0:
dp[j] += dp[j-1]
return dp[width-1]
时间复杂度:O(n × m),n、m 分别为obstacleGrid 长度和宽度
空间复杂度:O(m)
本题是 62.不同路径 的障碍版,整体思路大体一致。但就算是做过 62.不同路径,在做本题也会有感觉遇到障碍无从下手。其实只要考虑到,遇到障碍dp[i][j]保持0就可以了。也有一些小细节,例如:初始化的部分,很容易忽略了障碍之后应该都是0的情况。
1.2.3、64. 最小路径和
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
row = len(grid)
col = len(grid[0])
dp = [[0]*col for _ in range(row)]
# 初始化边缘,这些都是固定值,无需判断的
dp[0][0]=grid[0][0]
for i in range(1, row):
dp[i][0]=grid[i][0]+dp[i-1][0]
for i in range(1, col):
dp[0][i]=grid[0][i]+dp[0][i-1]
for i in range(1, row):
for j in range(1, col):
# 取最小值就行
dp[i][j]=min(dp[i-1][j], dp[i][j-1])+grid[i][j]
return dp[-1][-1]
1.2.4、343. 整数拆分
1 动态规划:
这道题求整数拆分后的最大乘积值的组合,使用动态规划来解,大的整数的拆分最大组合乘积等于其子整数的拆分最大组合乘积的乘积。
- 确定dp数组(dp table)以及下标的含义
dp[i]:拆分数字i,可以得到的最大乘积为dp[i]。 - 确定递推公式
递推公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
j * (i - j) 是单纯的把整数拆分为两个数相乘,而j * dp[i - j]是拆分成两个以及两个以上的个数相乘,加上dp[i],是跟上一次拆分组合比,每次保留最大值。 - dp的初始化
有的题解里会给出dp[0] = 1,dp[1] = 1的初始化,但解释比较牵强,主要还是因为这么初始化可以把题目过了。严格从dp[i]的定义来说,dp[0] dp[1] 就不应该初始化,也就是没有意义的数值。
拆分0和拆分1的最大乘积是多少?这是无解的。
这里只初始化dp[2] = 1,从dp[i]的定义来说,拆分数字2,得到的最大乘积是1。 - dp[i] 是依靠 dp[i - j]的状态,所以遍历i一定是从前向后遍历,先有dp[i - j]再有dp[i]。
枚举j的时候,是从1开始的。i是从3开始,这样dp[i - j]就是dp[2]正好可以通过我们初始化的数值求出来。
所以遍历顺序为:
for i in range(3, n+1):
for j in range(1, i//2+1):
dp[i] = max(dp[i], max(j*(i-j), j*dp[i-j]))
- 举例推导dp数组
举例当n为10 的时候,dp数组里的数值,如下:
class Solution:
def integerBreak(self, n: int) -> int:
# 数值加上一,索引才会到n
dp = [0]*(n+1)
# 初始化,从2开始,因为k>=2
dp[2] = 1
# 拆分成k,k>=2,2前面已经初始化,从3开始
for i in range(3, n+1):
# 拆分成j 和 i-j, 例如:1*(3-1) = 2*(3-2)所以整除以一半,再加上一。
for j in range(1, i//2+1):
# 遍历j时,更新dp[i]的最大值
# 这里j*(i-j)是将i拆分成两部分,j*dp[i-j]是将i拆分成两部分以上
dp[i] = max(dp[i], max(j*(i-j), j*dp[i-j]))
# dp中最后一个索引为n,即将n拆分的最大乘积
return dp[-1]
时间复杂度:O(n^2)
空间复杂度:O(n)
2 数学证明:
本题也可以用贪心,每次拆成n个3,如果剩下是4,则保留4,然后相乘,但是这个结论需要数学证明其合理性,可以自行查阅研究。
class Solution:
def integerBreak(self, n: int) -> int:
if n==2:
return 1
if n==3:
return 2
if n==4:
return 4
result = 1
while n>4:
result *= 3
n -= 3
result *= n
return result
1.2.5、96. 不同的二叉搜索树
动态规划:
- 确定dp数组(dp table)以及下标的含义
dp[i] : i个节点组成的二叉搜索树的个数为dp[i]。 - 确定递推公式
在上面的分析中,其实已经看出其递推关系, dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量]
j相当于是头结点的元素,从1遍历到i为止。
所以递推公式:dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量 - dp数组如何初始化
初始化,只需要初始化dp[0]就可以了,推导的基础,都是dp[0]。
那么dp[0]应该是多少呢?
从定义上来讲,空节点也是一棵二叉树,也是一棵二叉搜索树,这是可以说得通的。
从递归公式上来讲,dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量] 中以j为头结点左子树节点数量为0,也需要dp[以j为头结点左子树节点数量] = 1, 否则乘法的结果就都变成0了。
所以初始化dp[0] = 1 - 确定遍历顺序
首先一定是遍历节点数,从递归公式:dp[i] += dp[j - 1] * dp[i - j]可以看出,节点数为i的状态是依靠 i之前节点数的状态。
那么遍历i里面每一个数作为头结点的状态,用j来遍历。
for i in range(1, n+1):
for j in range(1, i+1):
dp[i] += dp[j-1]*dp[i-j]
- 举例推导dp数组
n为5时候的dp数组状态如图:
综上,代码如下:
class Solution:
def numTrees(self, n: int) -> int:
# 从0到n
dp = [0]*(n+1)
dp[0] = 1
for i in range(1, n+1):
for j in range(1, i+1):
# 左子树数量*右子树数量
# 每种情况都要累加起来
dp[i] += dp[j-1]*dp[i-j]
return dp[-1]
时间复杂度: O ( n 2 ) O(n^2) O(n2)
空间复杂度: O ( n ) O(n) O(n)
1.3、背包问题(背包DP)
背包问题,简单来说就是将单次、无限重复或者有限重复数量的物品,通过满足一定的题目需求,而放入有限大小的背包中。
而根据物品的三种不同规则,我们将分为三种背包问题:01背包,完全背包和多重背包。
它们的总结如下图:
但我们可以看到还有其他类型的背包在其中,但对于面试的话,其实掌握01背包,和完全背包,就够用了,最多可以再来一个多重背包。
至于其他的背包问题,面试几乎不会问,都是竞赛级别的了,leetcode上连多重背包的题目都没有,所以题库也告诉我们,01背包和完全背包就够用了。但如果你热爱学习,喜欢钻研,则一本背包问题的经典书籍送给你 背包问题九讲 2.0。
理解背包问题,01背包非常基础和重要,因为其他问题基本由此为基础衍生而来。我们先从纯背包问题开始,去理解和学习,再去延伸和转化到leetcode题目。
1.3.1、01背包
一次数量的物品,根据要求放入背包中。
暴力解法:
每一件物品其实只有两个状态:取或者不取
,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是 o ( 2 n ) o(2^n) o(2n),这里的n表示物品数量。
所以暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!
1.3.1.1、单次选择+最大价值
问题描述: 有 n n n件物品和一个最多能背重量为 w w w的背包。第 i i i件物品的重量是 w e i g h t [ i ] weight[i] weight[i],得到的价值是 v a l u e [ i ] value[i] value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
简单例子:
有 w = 4 w=4 w=4重量的背包
有 n = 3 n=3 n=3件物品,表示如下:
重量 weight[i] | 价值 value[i] | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
问背包能背的物品的最大价值是多少?
(1). 二维DP数组
动态规划五部曲
-
确定dp数组以及下标的含义
对于背包问题,有一种写法, 是使用二维数组,即 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示从下标为 [ 0 − i ] [0-i] [0−i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
-
确定递归公式
d p [ i ] [ j ] dp[i][j] dp[i][j]的含义为,从下标为 0 − i 0-i 0−i的物品里任取,放进容量为j的背包,价值总和最大是多少。
从两个方向可以推出 d p [ i ] [ j ] dp[i][j] dp[i][j]:- 不放物品i: 当物品i的重量大于背包j的重量时(即把背包清空也装不下),物品i无法放进背包中,所以被背包内的价值依然和前面相同,里面不放物品i的最大价值,此时 d p [ i ] [ j ] dp[i][j] dp[i][j]就是 d p [ i − 1 ] [ j ] dp[i - 1][j] dp[i−1][j]
- 放物品i: 想要放入一个物品,要此刻 d p [ i ] [ j ] dp[i][j] dp[i][j]的背包腾出物品i以及 w e i g h t [ i ] weight[i] weight[i]的重量,于是腾出后的价值为 d p [ i − 1 ] [ j − w e i g h t [ i ] ] dp[i-1][j-weight[i]] dp[i−1][j−weight[i]], i − 1 i-1 i−1则为腾出i之后前面 0 0 0 - i − 1 i-1 i−1的物品, j − w e i g h t [ i ] j-weight[i] j−weight[i]为腾出 w e i g h t [ i ] weight[i] weight[i]的重量。放入 w e i g h t [ i ] weight[i] weight[i]重量的物品后的总价值为 d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] dp[i-1][j-weight[i]]+value[i] dp[i−1][j−weight[i]]+value[i]。
-
数组的初始化
首先从 d p [ i ] [ j ] dp[i][j] dp[i][j]的定义出发,如果背包容量 j j j为 0 0 0的话,即 d p [ i ] [ 0 ] dp[i][0] dp[i][0],无论是选取哪些物品,背包价值总和一定为 0 0 0。如图:
再看其他情况,状态转移方程 d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i]) dp[i][j]=max(dp[i−1][j],dp[i−1][j−weight[i]]+value[i]),因为 i i i是由 i − 1 i-1 i−1推导而来,所以 i = 0 i=0 i=0一定要初始化。
即初始化 d p [ 0 ] [ j ] dp[0][j] dp[0][j],存放编号为0的物品时,各个容量的背包所能存放的最大值。
于是:- 当 w e i g h t [ 0 ] > j weight[0]>j weight[0]>j,说明当前背包的容量装不下第0个物品,那么 d p [ 0 ] [ j ] = 0 dp[0][j]=0 dp[0][j]=0
- 当 w e i g h t [ 0 ] < = j weight[0]<=j