动归题型分类:基础题目,背包问题,打家劫舍,股票问题,子序列问题。
误区:做动态规划的题,不能只关注递推公式。
解决动态规划本质的解题步骤:
1、我们在求解动态规划问题时,一般都会定义一个dp数组,我们要明确地知道dp数组的含义以及下标的含义。
2、递推公式。
3、dp数组如何初始化。
4、遍历顺序。是先遍历背包,还是先遍历物品?为什么有的题,遍历顺序要固定,有的题不管先遍历什么都能通过?采用前序遍历还是后序遍历?
5、打印dp数组。动态规划问题,代码一般比较简单,仅靠代码是很难看出问题的,这时候打印dp数组就显得尤为重要。
上述为动态规划五部曲,可以贯穿整个动态规划系列。
没看出来哪里用了动态规划
class Solution:
def fib(self, n: int) -> int:
dp = [i for i in range(n+1)]
for i in range(n+1):
if i > 1:
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
class Solution:
def fib(self, n: int) -> int:
# 排除 Corner Case
if n == 0:
return 0
# 创建 dp table
dp = [0] * (n + 1)
# 初始化 dp 数组
dp[0] = 0
dp[1] = 1
# 遍历顺序: 由前向后。因为后面要用到前面的状态
for i in range(2, n + 1):
# 确定递归公式/状态转移公式
dp[i] = dp[i - 1] + dp[i - 2]
# 返回答案
return dp[n]
后面这两个版本,都做了状态压缩,很多动态规划题目,都可以做状态压缩。
class Solution:
def fib(self, n: int) -> int:
if n <= 1:
return n
dp = [0, 1]
for i in range(2, n + 1):
total = dp[0] + dp[1]
dp[0] = dp[1]
dp[1] = total
return dp[1]
class Solution:
def fib(self, n: int) -> int:
if n <= 1:
return n
prev1, prev2 = 0, 1
for _ in range(2, n + 1):
curr = prev1 + prev2
prev1, prev2 = prev2, curr
return prev2
class Solution:
def fib(self, n: int) -> int:
if n <= 1 :
return n
dp = [0,1]
for i in range(2,n+1):
total = dp[0]+dp[1]
dp[0] = dp[1]
dp[1] = total
return dp[1]
class Solution:
def fib(self, n: int) -> int:
if n <= 1 :
return n
pre = 0
cur = 1
for i in range(2,n+1):
total = pre + cur
pre = cur
cur = total
return cur
想不出来,想不出递推公式。
这道题在递推公式的卡壳,根本原因就是没有弄明白dp[i]的含义。
脑子没转过去,已经想到了,当前步和前两步是有关系的,也去想了相加,但怎么就是被困住了,竟然开始纠结前两个值谁大的问题!
当前是N,对于N-1来说,只有一种方式,就是走一步,所以N-1到N的选择个数是确定的,就是N-1的选择数量。同理,从N-2走到N,也只有一种,就是走两步。
所以,N=N-1+N-2
class Solution:
def climbStairs(self, n: int) -> int:
if n == 1:
return 1
if n == 2:
return 2
dp = [0]*(n+1)
dp[1]=1
dp[2]=2
for i in range(3,n+1):
dp[i] = dp[i-1]+dp[i-2]
return dp[n]
代码随想录在本题提到的易错点我都没有犯,从一开始的思路,我就没想给dp[0]赋值,因为他是没有意义的,直接从dp[1] dp[2]开始计算就好了。
对于一步可以爬M步的情况,也是很好解决的。
动态规划(版本一)
# 空间复杂度为O(n)版本
class Solution:
def climbStairs(self, n: int) -> int:
if n <= 1:
return n
dp = [0] * (n + 1)
dp[1] = 1
dp[2] = 2
for i in range(3, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
动态规划(版本二)
# 空间复杂度为O(3)版本
class Solution:
def climbStairs(self, n: int) -> int:
if n <= 1:
return n
dp = [0] * 3
dp[1] = 1
dp[2] = 2
for i in range(3, n + 1):
total = dp[1] + dp[2]
dp[1] = dp[2]
dp[2] = total
return dp[2]
动态规划(版本三)
# 空间复杂度为O(1)版本
class Solution:
def climbStairs(self, n: int) -> int:
if n <= 1:
return n
prev1 = 1
prev2 = 2
for i in range(3, n + 1):
total = prev1 + prev2
prev1 = prev2
prev2 = total
return prev2
class Solution:
def climbStairs(self, n: int) -> int:
if n <= 1:
return 1
dp = [0]*3
dp[1] = 1
dp[2] = 2
for i in range(3,n+1):
total = dp[1]+dp[2]
dp[1] = dp[2]
dp[2] = total
return dp[2]
倒叙遍历,一维数组就够了,不知道想的对不对?
按照五部曲来说,dp数组含义就是从当前点出发到终点的最小值,递推公式是和遍历顺序相关的我觉得,因为要求最小花费,我觉得从前向后遍历,无法知道从当前处到达终点的最小值,因为还没遍历到。
如果没有明确遍历顺序,甚至还会有声明二维数组的想法,想到我们是知道,从终点的附件到达终点的最小花费的,那么就后序遍历,遍历到最开始,得到的就是最小值。
确定了遍历顺序,递推公式自然就有了,做一个min就好。初始化也是显然的。
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
n = len(cost)
dp = [0]*n
dp[n-1]=cost[n-1]
dp[n-2]=cost[n-2]
for i in range(n-3,-1,-1):
dp[i]=min(dp[i+1],dp[i+2])+cost[i]
return min(dp[0],dp[1])
按照代码随想录解答中的定义:到达第i台阶所花费的最少体力为dp[i]。
这样前序遍历就又可以了!初始化,dp[0]和dp[1]初始化为0就可以,因为这都是可以选择的起点。
也是一种思路!
动态规划(版本一)
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
dp = [0] * (len(cost) + 1)
dp[0] = 0 # 初始值,表示从起点开始不需要花费体力
dp[1] = 0 # 初始值,表示经过第一步不需要花费体力
for i in range(2, len(cost) + 1):
# 在第i步,可以选择从前一步(i-1)花费体力到达当前步,或者从前两步(i-2)花费体力到达当前步
# 选择其中花费体力较小的路径,加上当前步的花费,更新dp数组
dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])
return dp[len(cost)] # 返回到达楼顶的最小花费
动态规划(版本二)
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
dp0 = 0 # 初始值,表示从起点开始不需要花费体力
dp1 = 0 # 初始值,表示经过第一步不需要花费体力
for i in range(2, len(cost) + 1):
# 在第i步,可以选择从前一步(i-1)花费体力到达当前步,或者从前两步(i-2)花费体力到达当前步
# 选择其中花费体力较小的路径,加上当前步的花费,得到当前步的最小花费
dpi = min(dp1 + cost[i - 1], dp0 + cost[i - 2])
dp0 = dp1 # 更新dp0为前一步的值,即上一次循环中的dp1
dp1 = dpi # 更新dp1为当前步的最小花费
return dp1 # 返回到达楼顶的最小花费
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
n = len(cost)
dp = [0]*(n+1)
dp[0] = 0
dp[1] = 0
for i in range(2,n+1):
dp[i] = min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2])
return dp[n]
动态规划系列1总结
第一次debug未过主要有以下两点:
1、因为题目未说清楚,以为当m=n=1时,结果为0,没想到是1
2、初始化左边界和上边界,一开始写错了,应该全部初始化为,而不是累加。
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [[0] * n for _ in range(m)]
dp[0][0]=1
for i in range(1,m):
dp[i][0] = 1
for i in range(1,n):
dp[0][i] = 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[m-1][n-1]
此题也要弄明白dp[i]的含义,是从0走到这里,一共有多少种不同的路径,而不是走了多少步!所以是前两个直接相加,不是错误的+1步。
这道题中有很多值得深挖的地方,详情参加代码随想录的解答文章。
不同路径
大致概括有如下几个方面:深搜会超时,那么深搜的时间复杂度是什么?除了动态规划,还有一种基于组合数的思路,这种思路也有要注意的点,就是要防止溢出,在累乘的过程中不断除以分母。还有就是,基于动态规划的方法,有一个可以节省存储空间的一维数组的方法,类似于滚动数组,也可以学习。
动态规划(版本一)
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
# 创建一个二维列表用于存储唯一路径数
dp = [[0] * n for _ in range(m)]
# 设置第一行和第一列的基本情况
for i in range(m):
dp[i][0] = 1
for j in range(n):
dp[0][j] = 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[m - 1][n - 1]
动态规划(版本二)
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
# 创建一个一维列表用于存储每列的唯一路径数
dp = [1] * n
# 计算每个单元格的唯一路径数
for j in range(1, m):
for i in range(1, n):
dp[i] += dp[i - 1]
# 返回右下角单元格的唯一路径数
return dp[n - 1]
数论
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
numerator = 1 # 分子
denominator = m - 1 # 分母
count = m - 1 # 计数器,表示剩余需要计算的乘积项个数
t = m + n - 2 # 初始乘积项
while count > 0:
numerator *= t # 计算乘积项的分子部分
t -= 1 # 递减乘积项
while denominator != 0 and numerator % denominator == 0:
numerator //= denominator # 约简分子
denominator -= 1 # 递减分母
count -= 1 # 计数器减1,继续下一项的计算
return numerator # 返回最终的唯一路径数
过。
基于上一题稍作修改,感觉不是很难。
Bebug了几次,主要用于修改边界和初始判断条件。
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
m = len(obstacleGrid)
n = len(obstacleGrid[0])
if obstacleGrid[0][0]==1 or obstacleGrid[m-1][n-1]==1:
return 0
dp = [[0] * n for _ in range(m)]
dp[0][0]=1
for i in range(1,m):
if obstacleGrid[i][0]==1 :
break
dp[i][0] = 1
for i in range(1,n):
if obstacleGrid[0][i]==1 :
break
dp[0][i] = 1
for i in range(1,m):
for j in range(1,n):
if obstacleGrid[i][j] != 1 :
dp[i][j] = dp[i-1][j]+dp[i][j-1]
return dp[m-1][n-1]
本题的难点在于,如果在本题中使用一维数组,在递推公式上,要谨慎编写。
动态规划(版本一)
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid):
m = len(obstacleGrid)
n = len(obstacleGrid[0])
if obstacleGrid[m - 1][n - 1] == 1 or obstacleGrid[0][0] == 1:
return 0
dp = [[0] * n for _ in range(m)]
for i in range(m):
if obstacleGrid[i][0] == 0: # 遇到障碍物时,直接退出循环,后面默认都是0
dp[i][0] = 1
else:
break
for j in range(n):
if obstacleGrid[0][j] == 0:
dp[0][j] = 1
else:
break
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]
return dp[m - 1][n - 1]
动态规划(版本三),一维数组省空间
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid):
if obstacleGrid[0][0] == 1:
return 0
dp = [0] * len(obstacleGrid[0]) # 创建一个一维列表用于存储路径数
# 初始化第一行的路径数
for j in range(len(dp)):
if obstacleGrid[0][j] == 1:
dp[j] = 0
elif j == 0:
dp[j] = 1
else:
dp[j] = dp[j - 1]
# 计算其他行的路径数
for i in range(1, len(obstacleGrid)):
for j in range(len(dp)):
if obstacleGrid[i][j] == 1:
dp[j] = 0
elif j != 0:
dp[j] = dp[j] + dp[j - 1]
return dp[-1] # 返回最后一个元素,即终点的路径数
过。
递推数组是一维的做法,最关键的一点就是数组是在迭代之间传递的,
dp[j] = dp[j] + dp[j - 1] , 第一个dp代表当前值,第二个dp代表当前值的上方,第三个dp代表当前值的左方。
很巧妙!
自己写了个递归的,但是发现数学逻辑出现错误了,两数相近时,乘积最大,前提是只分解为2个数,但这道题显然不满足这个前提,反例:8。最大值是332,而不是4*4。
没思路没思路。
此题的难点在于,dp为一维数组,但是循环却要两层。
递推公式也不好想,将n拆为 i 和 n-i 后,是取 i*(n-i) 和 i*dp[n-i] 和dp[i] 的最大值,dp[n-i] 代表着拆分为多个值的情况,还要和dp[i]比较是因为,i 是根据我们的循环控制的,不同的 i 对应着不同的值,要取一个最大的。
同时,代码随想录中还提到了一种,编写不清晰,初始化讲不通,仅仅是能运气好通过的代码,也要学习。
同样本题还有一个剪枝的小技巧,这个我想到了。
另外,本题贪心的数学依据,代码随想录并未给出。
整数拆分
动态规划(版本一)
class Solution:
# 假设对正整数 i 拆分出的第一个正整数是 j(1 <= j < i),则有以下两种方案:
# 1) 将 i 拆分成 j 和 i−j 的和,且 i−j 不再拆分成多个正整数,此时的乘积是 j * (i-j)
# 2) 将 i 拆分成 j 和 i−j 的和,且 i−j 继续拆分成多个正整数,此时的乘积是 j * dp[i-j]
def integerBreak(self, n):
dp = [0] * (n + 1) # 创建一个大小为n+1的数组来存储计算结果
dp[2] = 1 # 初始化dp[2]为1,因为当n=2时,只有一个切割方式1+1=2,乘积为1
# 从3开始计算,直到n
for i in range(3, n + 1):
# 遍历所有可能的切割点
for j in range(1, i // 2 + 1):
# 计算切割点j和剩余部分(i-j)的乘积,并与之前的结果进行比较取较大值
dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j))
return dp[n] # 返回最终的计算结果
贪心
class Solution:
def integerBreak(self, n):
if n == 2: # 当n等于2时,只有一种拆分方式:1+1=2,乘积为1
return 1
if n == 3: # 当n等于3时,只有一种拆分方式:1+1+1=3,乘积为1
return 2
if n == 4: # 当n等于4时,有两种拆分方式:2+2=4和1+1+1+1=4,乘积都为4
return 4
result = 1
while n > 4:
result *= 3 # 每次乘以3,因为3的乘积比其他数字更大
n -= 3 # 每次减去3
result *= n # 将剩余的n乘以最后的结果
return result
注意是三个数比较,求Max,别忘了 dp[i]
class Solution:
def integerBreak(self, n: int) -> int:
dp = [0]*(n+1)
dp[1] = 0
dp[2] = 1
for i in range(3,n+1):
for j in range(1,i):
dp[i] = max(dp[i],j*(i-j),j*dp[i-j])
return dp[n]
剪枝:本题的计算过程中,有很多重复的地方,因为我们一直求得都是最大值,很多值其实没必要计算,比如按照严谨的递归逻辑,是没有理由不考虑 j > i /2 的情况的,但是分解大数肯定会得到更大的值,当该数大于4时,所以是可以做出如下的剪枝的。
class Solution:
def integerBreak(self, n: int) -> int:
dp = [0]*(n+1)
dp[1] = 0
dp[2] = 1
for i in range(3,n+1):
for j in range(1,i//2+1):
dp[i] = max(dp[i],j*(i-j),j*dp[i-j])
return dp[n]
依旧想不出递推公式。
差一点就推出来了。
本题的关键。
二叉搜索树的子树也都是二叉搜索树。
递推公式的本质,在于选择 [1,n] 中哪个数作为头结点, 假设选择 i 。
那么左边一定都是比 i 小的数,一共有 i-1 个。
右边一定都是比 i 大的数,一共有 n-i 个。
最后,两边的dp相乘,得出选 i 为头结点时,不同的二叉搜索树的数量。
class Solution:
def numTrees(self, n: int) -> int:
dp = [0] * (n + 1) # 创建一个长度为n+1的数组,初始化为0
dp[0] = 1 # 当n为0时,只有一种情况,即空树,所以dp[0] = 1
for i in range(1, n + 1): # 遍历从1到n的每个数字
for j in range(1, i + 1): # 对于每个数字i,计算以i为根节点的二叉搜索树的数量
dp[i] += dp[j - 1] * dp[i - j] # 利用动态规划的思想,累加左子树和右子树的组合数量
return dp[n] # 返回以1到n为节点的二叉搜索树的总数量
非常好的一道题,递推公式很难想到,想到之后,初始化和区间编写细节,也很容易出错。
class Solution:
def numTrees(self, n: int) -> int:
if n <= 1 :
return 1
dp = [0]*(n+1)
dp[0] = 1
dp[1] = 1
for i in range(2,n+1):
for j in range(i):
dp[i] += dp[j]*dp[i-j-1]
return dp[n]
本周的题,我主要失败在递推公式找不到上,而且两题均是只差一点就找到了!
动态规划系列2总结
应对大厂面试,01背包和完全背包就够用了,目前在力扣上也找不到多重背包的题目。
注意,下面所涉及的一些方法,都是01背包的。
对于01背包问题,要先了解其暴力解法,每种物品只有两个状态,【取,不取】,那么我们用回溯算法,遍历所有状态即可。
dp数组的含义:dp[i][j] 下标为[0,i]的物品,任取,放进容量为 j 的背包里。
dp[i][j] 的状态,就取决于,目前,我们放不放物品 i ,放是一个状态,不放又是一个状态。
不放物品 i : dp[i][j] = dp[i-1][j]
放物品 i : dp[i][j] = value(i) + dp[i-1][j-weight[i]]
递推公式:dp[i][j] = max( dp[i-1][j] , value(i) + dp[i-1][j-weight[i]] )
确定如何初始化很重要,可以画一下dp二维数组。
遍历顺序也很关键!二维dp数组实现的01背包,两层for循环是可以颠倒的,先遍历物品,或是先遍历背包,都可以。(在这里,dp数组的行是物品,列是背包容量)。因为根据递推公式,当前物品,是由上方数值和左上方数值,推导出来的。
故而,两层for循环的顺序,不影响。
那么使用一维dp数组来实现01背包呢?注意,这里的遍历顺序是有讲究的。
为什么可以进行降维,用一维滚动数组来实现呢?
注意我们的递推公式,dp[i][j] = max( dp[i-1][j] , value(i) + dp[i-1][j-weight[i]] )
第 i 层 只和 i-1 层有关,我们可以将 i-1 层的数据,拷贝到第 i 层,然后进行滚动计算,这就是为什么使用一维数组。
一维 dp[j] 含义:容量为 j 的背包所能装的最大价值。同样,状态取决于放不放物品 i ,目前,我们放不放物品 i ,放是一个状态,不放又是一个状态。
不放物品 i : dp[j] = dp[j] (因为这里我们是拷贝的,这里其实是上一层的数据)
放物品 i : dp[j] = dp[j-weight(i)]+value(i)
递推公式 : dp[j] = max( dp[j] , dp[j-weight(i)]+value(i) )
dp数组的初始化:dp[0] = 0,dp[i]要初始化为非负数里的最小值,因为后续会取max,如果初始化为一个正数,可能会影响max操作。所以全部初始化为0.
遍历顺序:先遍历物品,在遍历背包,背包要倒序遍历。(此遍历顺序唯一)
为什么要倒序?保证每个物品只被添加一次。如果是正序,对于最小的物品,就会被多次添加,代码随想录给出的例子很形象。因为我们的数据是从上一层拷贝来的,更新时需要用到此数据,而正序遍历会将数据破坏掉,后面的值无法获得准确的上一层的值,所以也要倒序遍历。
倒序遍历的原因是,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。
为什么二维数据可以正序遍历?因为二维数据的每一行都是解耦的,给 i 行赋值,不会影响我们后面去调用 i-1 行的操作。
为什么必须先遍历物品,再遍历背包?颠倒顺序后,先遍历背包,再遍历物品,但因为我们的背包是倒序遍历的,会导致每个背包只放入一个物品。
无参数版
def test_2_wei_bag_problem1():
weight = [1, 3, 4]
value = [15, 20, 30]
bagweight = 4
# 二维数组
dp = [[0] * (bagweight + 1) for _ in range(len(weight))]
# 初始化
for j in range(weight[0], bagweight + 1):
dp[0][j] = value[0]
# weight数组的大小就是物品个数
for i in range(1, len(weight)): # 遍历物品
for j in range(bagweight + 1): # 遍历背包容量
if j < weight[i]:
dp[i][j] = dp[i - 1][j]
else:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
print(dp[len(weight) - 1][bagweight])
test_2_wei_bag_problem1()
有参数版
def test_2_wei_bag_problem1(weight, value, bagweight):
# 二维数组
dp = [[0] * (bagweight + 1) for _ in range(len(weight))]
# 初始化
for j in range(weight[0], bagweight + 1):
dp[0][j] = value[0]
# weight数组的大小就是物品个数
for i in range(1, len(weight)): # 遍历物品
for j in range(bagweight + 1): # 遍历背包容量
if j < weight[i]:
dp[i][j] = dp[i - 1][j]
else:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
return dp[len(weight) - 1][bagweight]
if __name__ == "__main__":
weight = [1, 3, 4]
value = [15, 20, 30]
bagweight = 4
result = test_2_wei_bag_problem1(weight, value, bagweight)
print(result)
无参版
def test_1_wei_bag_problem():
weight = [1, 3, 4]
value = [15, 20, 30]
bagWeight = 4
# 初始化
dp = [0] * (bagWeight + 1)
for i in range(len(weight)): # 遍历物品
for j in range(bagWeight, weight[i] - 1, -1): # 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
print(dp[bagWeight])
test_1_wei_bag_problem()
有参版
def test_1_wei_bag_problem(weight, value, bagWeight):
# 初始化
dp = [0] * (bagWeight + 1)
for i in range(len(weight)): # 遍历物品
for j in range(bagWeight, weight[i] - 1, -1): # 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
return dp[bagWeight]
if __name__ == "__main__":
weight = [1, 3, 4]
value = [15, 20, 30]
bagweight = 4
result = test_1_wei_bag_problem(weight, value, bagweight)
print(result)
一开始的错误思路:
class Solution:
def canPartition(self, nums: List[int]) -> bool:
n = len(nums)
if n == 1:
return False
sumsum = sum(nums)
# weight = [1 1 1 1]
# value = nums
# 总重 n
dp = [0]*n
for i in range(n):
for j in range(n-1,0,-1):
dp[j] = dp[j-1]+nums[i]
if 2*dp[j] == sumsum :
return True
return False
调了半天,终于过了
class Solution:
def canPartition(self, nums: List[int]) -> bool:
n = len(nums)
sumsum = sum(nums)
if sumsum % 2 == 1:
return False
sum_weight = int(sumsum / 2)
# weight = nums
# value = nums
# sum_weight = sum(nums)/2
dp = [0]*(sum_weight+1)
for i in range(n):
for j in range(sum_weight,nums[i]-1,-1):
dp[j] = max(dp[j-nums[i]]+nums[i],dp[j])
if dp[-1] == sum_weight :
return True
else :
return False
之前我一直没真正写过01背包的代码,这次写的时候发现了几个被我忽视的点:
1、dp数组,不管是二维还是一维的,其长度,均为weight+1 !!! 不是 weight 。
这个也好理解,完全按照背包重量的定义来的,0的时候表明背包最大承重为0.
2、倒序遍历的末尾值: 是 weight[i]-1 , 这表明最多只能遍历到 weight[i] ,表明刚好放进物品 i , 剩余重量为 0 ,不要忘记减一
要明确本题中我们要使用的是01背包,因为元素我们只能用一次。
初始化,重要:
如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。
这样才能让dp数组在递推的过程中取得最大的价值,而不是被初始值覆盖了。
本题题目中 只包含正整数的非空数组,所以非0下标的元素初始化为0就可以了。
本题能用01背包的原因就是,本题让每个物品的weight和value都是nums数组的值,当背包装完后,如果:dp[target]=target,就说明背包被装满了,如果不等于,说明没装满。
二维DP版
class Solution:
def canPartition(self, nums: List[int]) -> bool:
total_sum = sum(nums)
if total_sum % 2 != 0:
return False
target_sum = total_sum // 2
dp = [[False] * (target_sum + 1) for _ in range(len(nums) + 1)]
# 初始化第一行(空子集可以得到和为0)
for i in range(len(nums) + 1):
dp[i][0] = True
for i in range(1, len(nums) + 1):
for j in range(1, target_sum + 1):
if j < nums[i - 1]:
# 当前数字大于目标和时,无法使用该数字
dp[i][j] = dp[i - 1][j]
else:
# 当前数字小于等于目标和时,可以选择使用或不使用该数字
dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i - 1]]
return dp[len(nums)][target_sum]
一维DP版
class Solution:
def canPartition(self, nums: List[int]) -> bool:
total_sum = sum(nums)
if total_sum % 2 != 0:
return False
target_sum = total_sum // 2
dp = [False] * (target_sum + 1)
dp[0] = True
for num in nums:
# 从target_sum逆序迭代到num,步长为-1
for i in range(target_sum, num - 1, -1):
dp[i] = dp[i] or dp[i - num]
return dp[target_sum]
过。
直接从上一题改过来的代码错误,这题很难用动态规划,要用回溯。先跳过吧。等复习动态规划的时候,可以看看这题。
同样很难用动态规划,要用回溯。先跳过吧。等复习动态规划的时候,可以看看这题。
想不到怎么用01背包。
只想到了求和,除以2,但是怎么用背包还是不懂。
代码随想录给出的思路:
本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了。
一开始想不太懂,但是再想想就明白了!这两堆石头,根本不用管哪边的石头多,因为相撞后,残留部分会回到原集合中。然后继续相撞!!!
class Solution:
def lastStoneWeightII(self, stones: List[int]) -> int:
n = len(stones)
sumsum = sum(stones)
sum_weight = sumsum // 2
# weight = nums
# value = nums
# sum_weight = sum(nums)/2
dp = [0]*(sum_weight+1)
for i in range(n):
for j in range(sum_weight,stones[i]-1,-1):
dp[j] = max(dp[j-stones[i]]+stones[i],dp[j])
return abs(sumsum-2*dp[-1])
二维DP
class Solution:
def lastStoneWeightII(self, stones: List[int]) -> int:
total_sum = sum(stones)
target = total_sum // 2
# 创建二维dp数组,行数为石头的数量加1,列数为target加1
# dp[i][j]表示前i个石头能否组成总重量为j
dp = [[False] * (target + 1) for _ in range(len(stones) + 1)]
# 初始化第一列,表示总重量为0时,前i个石头都能组成
for i in range(len(stones) + 1):
dp[i][0] = True
for i in range(1, len(stones) + 1):
for j in range(1, target + 1):
# 如果当前石头重量大于当前目标重量j,则无法选择该石头
if stones[i - 1] > j:
dp[i][j] = dp[i - 1][j]
else:
# 可选择该石头或不选择该石头
dp[i][j] = dp[i - 1][j] or dp[i - 1][j - stones[i - 1]]
# 找到最大的重量i,使得dp[len(stones)][i]为True
# 返回总重量减去两倍的最接近总重量一半的重量
for i in range(target, -1, -1):
if dp[len(stones)][i]:
return total_sum - 2 * i
return 0
一维DP
class Solution:
def lastStoneWeightII(self, stones):
total_sum = sum(stones)
target = total_sum // 2
dp = [False] * (target + 1)
dp[0] = True
for stone in stones:
for j in range(target, stone - 1, -1):
# 判断当前重量是否可以通过选择之前的石头得到或选择当前石头和之前的石头得到
dp[j] = dp[j] or dp[j - stone]
for i in range(target, -1, -1):
if dp[i]:
# 返回剩余石头的重量,即总重量减去两倍的最接近总重量一半的重量
return total_sum - 2 * i
return 0
过。
本周就讲了01背包问题,01背包问题全部用一维DP数组求解。
动态规划系列3总结
不会,没思路。
转换思路,太重要了!前面放加法和放减法,本质上就是将整个集合,分为了左右两个集合,和前面两题思路又类似了!
回溯版
class Solution:
def backtracking(self, candidates, target, total, startIndex, path, result):
if total == target:
result.append(path[:]) # 将当前路径的副本添加到结果中
# 如果 sum + candidates[i] > target,则停止遍历
for i in range(startIndex, len(candidates)):
if total + candidates[i] > target:
break
total += candidates[i]
path.append(candidates[i])
self.backtracking(candidates, target, total, i + 1, path, result)
total -= candidates[i]
path.pop()
def findTargetSumWays(self, nums: List[int], target: int) -> int:
total = sum(nums)
if target > total:
return 0 # 此时没有方案
if (target + total) % 2 != 0:
return 0 # 此时没有方案,两个整数相加时要注意数值溢出的问题
bagSize = (target + total) // 2 # 转化为组合总和问题,bagSize就是目标和
# 以下是回溯法代码
result = []
nums.sort() # 需要对nums进行排序
self.backtracking(nums, bagSize, 0, 0, [], result)
return len(result)
二维DP
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
total_sum = sum(nums) # 计算nums的总和
if abs(target) > total_sum:
return 0 # 此时没有方案
if (target + total_sum) % 2 == 1:
return 0 # 此时没有方案
target_sum = (target + total_sum) // 2 # 目标和
# 创建二维动态规划数组,行表示选取的元素数量,列表示累加和
dp = [[0] * (target_sum + 1) for _ in range(len(nums) + 1)]
# 初始化状态
dp[0][0] = 1
# 动态规划过程
for i in range(1, len(nums) + 1):
for j in range(target_sum + 1):
dp[i][j] = dp[i - 1][j] # 不选取当前元素
if j >= nums[i - 1]:
dp[i][j] += dp[i - 1][j - nums[i - 1]] # 选取当前元素
return dp[len(nums)][target_sum] # 返回达到目标和的方案数
一维DP
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
total_sum = sum(nums) # 计算nums的总和
if abs(target) > total_sum:
return 0 # 此时没有方案
if (target + total_sum) % 2 == 1:
return 0 # 此时没有方案
target_sum = (target + total_sum) // 2 # 目标和
dp = [0] * (target_sum + 1) # 创建动态规划数组,初始化为0
dp[0] = 1 # 当目标和为0时,只有一种方案,即什么都不选
for num in nums:
for j in range(target_sum, num - 1, -1):
dp[j] += dp[j - num] # 状态转移方程,累加不同选择方式的数量
return dp[target_sum] # 返回达到目标和的方案数
我明白了!看了代码之后就懂了!就是背包,二维的定义,也完全符合背包定义。
二维DP
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
total_sum = sum(nums) # 计算nums的总和
if abs(target) > total_sum:
return 0 # 此时没有方案
if (target + total_sum) % 2 == 1:
return 0 # 此时没有方案
target_sum = (target + total_sum) // 2 # 目标和
# 创建二维动态规划数组,行表示选取的元素数量,列表示累加和
dp = [[0] * (target_sum + 1) for _ in range(len(nums) + 1)]
# 初始化状态
dp[0][0] = 1
# 动态规划过程
for i in range(1, len(nums) + 1):
for j in range(target_sum + 1):
dp[i][j] = dp[i - 1][j] # 不选取当前元素
if j >= nums[i - 1]:
dp[i][j] += dp[i - 1][j - nums[i - 1]] # 选取当前元素
return dp[len(nums)][target_sum] # 返回达到目标和的方案数
一维DP
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
total_sum = sum(nums) # 计算nums的总和
if abs(target) > total_sum:
return 0 # 此时没有方案
if (target + total_sum) % 2 == 1:
return 0 # 此时没有方案
target_sum = (target + total_sum) // 2 # 目标和
dp = [0] * (target_sum + 1) # 创建动态规划数组,初始化为0
dp[0] = 1 # 当目标和为0时,只有一种方案,即什么都不选
for num in nums:
for j in range(target_sum, num - 1, -1):
dp[j] += dp[j - num] # 状态转移方程,累加不同选择方式的数量
return dp[target_sum] # 返回达到目标和的方案数
下面是错误的代码,里面坑很多
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
total = sum(nums)
# 这里的判断注意加绝对值,因为 target 也可能是负数
if abs(target) > total :
return 0
temp = total + target
if temp % 2 == 1:
return 0
positive = temp // 2
n = len(nums)
dp = [[0]*(positive+1) for _ in range(n+1)]
dp[0][0] = 1
# 为什么这样初始化,不对,因为给出的 nums 数组可能含有很多 0
# 这样当 目标整数和 为0时,即 j = 0 的情况,不是只有一种可能
# 如果题目说明,数组中均为正整数,这样初始化就没问题
for i in range(n+1):
dp[i][0] = 1
for i in range(1,n+1):
for j in range(1,positive+1):
# 先赋值
dp[i][j] = dp[i-1][j]
if j >= nums[i-1]:
dp[i][j] += dp[i-1][j-nums[i-1]]
print(dp)
return dp[-1][-1]
详细注释代码
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
total = sum(nums)
# 这里的判断注意加绝对值,因为 target 也可能是负数
if abs(target) > total :
return 0
temp = total + target
if temp % 2 == 1:
return 0
positive = temp // 2
n = len(nums)
dp = [[0]*(positive+1) for _ in range(n+1)]
dp[0][0] = 1
# 为什么这样初始化,不对,因为给出的 nums 数组可能含有很多 0
# 这样当 目标整数和 为0时,即 j = 0 的情况,不是只有一种可能
# 如果题目说明,数组中均为正整数,这样初始化就没问题
'''
for i in range(n+1):
dp[i][0] = 1
'''
for i in range(1,n+1):
# 所以本题一定要注意,这里 j 不是从 1 开始遍历,而是从 0
for j in range(positive+1):
# 先赋值,不管用不用 nums[i-1] , dp[i][j]的值,至少是dp[i-1][j]
dp[i][j] = dp[i-1][j]
# 如果nums[i-1]能用,那么dp[i][j]是在dp[i-1][j]的基础上加
# 而不是其他题目,做的if-else逻辑
if j >= nums[i-1]:
dp[i][j] += dp[i-1][j-nums[i-1]]
print(dp)
return dp[-1][-1]
一维数组,带注释版,滚动数组,自动完成了拷贝和赋值的操作,这是和二维DP数组的区别!
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
total = sum(nums)
# 这里的判断注意加绝对值,因为 target 也可能是负数
if abs(target) > total :
return 0
temp = total + target
if temp % 2 == 1:
return 0
positive = temp // 2
n = len(nums)
dp = [0]*(positive+1)
dp[0] = 1
for i in range(n):
# 注意这里,滚动数组自然完成了赋值的操作!
for j in range(positive,-1,-1):
if j >= nums[i]:
dp[j] += dp[j-nums[i]]
return dp[-1]
不会,没思路
还是对01背包的应用题不太了解,看了解答之后豁然开朗,其实想到了这是一个二维度的背包,但是再下一步就不懂了,对dp数组的定义还是掌握不牢。
DP(版本一)
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
dp = [[0] * (n + 1) for _ in range(m + 1)] # 创建二维动态规划数组,初始化为0
for s in strs: # 遍历物品
zeroNum = s.count('0') # 统计0的个数
oneNum = len(s) - zeroNum # 统计1的个数
for i in range(m, zeroNum - 1, -1): # 遍历背包容量且从后向前遍历
for j in range(n, oneNum - 1, -1):
dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1) # 状态转移方程
return dp[m][n]
DP(版本二)
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
dp = [[0] * (n + 1) for _ in range(m + 1)] # 创建二维动态规划数组,初始化为0
# 遍历物品
for s in strs:
ones = s.count('1') # 统计字符串中1的个数
zeros = s.count('0') # 统计字符串中0的个数
# 遍历背包容量且从后向前遍历
for i in range(m, zeros - 1, -1):
for j in range(n, ones - 1, -1):
dp[i][j] = max(dp[i][j], dp[i - zeros][j - ones] + 1) # 状态转移方程
return dp[m][n]
传统三维背包代码,有助于理解01背包
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
dp = [[[0] * (n + 1) for _ in range(m + 1)] for _ in range(len(strs)+1)]
# 遍历物品
for k in range(1,len(strs)+1):
s = strs[k-1]
ones = s.count('1') # 统计字符串中1的个数
zeros = s.count('0') # 统计字符串中0的个数
# 遍历背包容量且从后向前遍历
for i in range(m+1):
for j in range(n+1):
# 一维滚动数组下,可以不用写判断,可以直接让 j >= weight[i] 来作为循环的结束点(倒序遍历)
# 但是在最原始的二维DP数组写法下,顺序遍历必须是全区间遍历!
# 如果 j < weight[i] ,也是要赋值的,直接赋值为上一个值
# 因为一维中,滚动数组的内在赋值覆盖逻辑,所以可以省去这一步!
if i >= zeros and j >= ones :
# 这里注意啊,两个都是 k-1 , 而不是 k
dp[k][i][j] = max(dp[k-1][i][j], dp[k-1][i - zeros][j - ones] + 1) # 状态转移方程
else :
dp[k][i][j] = dp[k-1][i][j]
return dp[len(strs)][m][n]
纯完全背包问题,两个循环的顺序是可以颠倒的。
不同于01背包的是,采用前序遍历,这样物品就可以使用无数次了!
代码随想录的博客写的很好!
完全背包
先遍历物品,再遍历背包(有参版)
def test_CompletePack(weight, value, bagWeight):
dp = [0] * (bagWeight + 1)
for i in range(len(weight)): # 遍历物品
for j in range(weight[i], bagWeight + 1): # 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
return dp[bagWeight]
if __name__ == "__main__":
weight = [1, 3, 4]
value = [15, 20, 30]
bagWeight = 4
result = test_CompletePack(weight, value, bagWeight)
print(result)
先遍历背包,再遍历物品(有参版)
def test_CompletePack(weight, value, bagWeight):
dp = [0] * (bagWeight + 1)
for j in range(bagWeight + 1): # 遍历背包容量
for i in range(len(weight)): # 遍历物品
if j - weight[i] >= 0:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
return dp[bagWeight]
if __name__ == "__main__":
weight = [1, 3, 4]
value = [15, 20, 30]
bagWeight = 4
result = test_CompletePack(weight, value, bagWeight)
print(result)
01背包
for i in range(n):
for j in range(m):
if j >= weight[i] :
dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])
else :
dp[i][j] = dp[i-1][j]
完全背包
for i in range(n):
for j in range(m):
if j >= weight[i] :
# 区别仅仅就在这一句话
dp[i][j] = max(dp[i-1][j],dp[i][j-weight[i]]+value[i])
else :
dp[i][j] = dp[i-1][j]
再次强调,二维DP数组,原始背包代码,要遍历所有值,判断当前 j 和 nums[i] 的大小关系,不能像一维DP数组那样写,缩小遍历范围,因为一维DP数组的循环赋值,以及加和操作,在 j < nums[i] 的时候,自然就等于上一个值,而二维DP数组,不做赋值,就会是0!!!
01背包
for i in range(n):
for j in range(m-1,weight[i]-1,-1):
dp[j] = max(dp[j],dp[j-weight[i]]+value[i])
完全背包
for i in range(n):
for j in range(weight[i],m):
dp[j] = max(dp[j],dp[j-weight[i]]+value[i])
先遍历物品,再遍历背包,得到的是组合数,只有 【1,2】这种情况,没有【2,1】这种情况。而先遍历背包,再遍历物品,则每个背包下,都有【1,2】【2,1】这两种组合,所以这是求得排列数。
不管是组合数,还是排列数,这是只有完全背包才有的问题,因为数量是无限的,才要考虑顺序问题。
01背包是没有组合数or排列数的问题的。
就是一个完全背包问题,递推公式是前面接触过的,各个值的加和。遍历顺序就采用先遍历物品,再遍历背包。
看了代码随想录的解答才知道,本题的遍历顺序有考究!必须先遍历物品,再遍历背包,因为这题求得是组合数,不强调元素的顺序。
简单来讲,先遍历物品,再遍历背包,得到的是组合数,只有 【1,2】这种情况,没有【2,1】这种情况。而先遍历背包,再遍历物品,则每个背包下,都有【1,2】【2,1】这两种组合,所以这是求得排列数。
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
dp = [0]*(amount + 1)
dp[0] = 1
# 遍历物品
for i in range(len(coins)):
# 遍历背包
for j in range(coins[i], amount + 1):
dp[j] += dp[j - coins[i]]
return dp[amount]
过。
标准二维DP数组写法,本题的注意事项,和上面的“494 目标和”一致,都是累加的递推公式,初始化上,都要注意,j 从0开始,而不是从1,并且要先赋值再判断。
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
n = len(coins)
dp = [[0]*(amount+1) for _ in range(n+1)]
dp[0][0] = 1
for i in range(1,n+1):
for j in range(amount+1):
dp[i][j] = dp[i-1][j]
if j >= coins[i-1]:
dp[i][j] += dp[i][j-coins[i-1]]
return dp[-1][-1]
动态规划系列4总结