当前位置: 首页 > news >正文

算法刷题总结 (四) 动态规划

算法总结4 动态规划

  • 一、动态规划
    • 1.1、基础问题1
        • 1.1.1、509. 斐波那契数列
        • 1.1.2、70. 爬楼梯
        • 1.1.3、746. 使用最小花费爬楼梯
    • 1.2、基础问题2
        • 1.2.1、62. 不同路径
        • 1.2.2、63. 不同路径Ⅱ
        • 1.2.3、343. 整数拆分
        • 1.2.4、96. 不同的二叉搜索树
    • 1.3、背包问题
      • 1.3.1、01背包
        • 1.3.1.1、单次选择+最大价值
        • 1.3.1.2、单次选择+最大重量
          • 416. 分割等和子集
          • 1049. 最后一块石头的重量 II
          • 474. 一和零(双维度背包)
        • 1.3.1.3、单次选择+装满可能性总数
          • 494. 目标和
      • 1.3.2、完全背包
        • 1.3.2.1、重复选择+最大价值
        • 1.3.2.2、重复选择+组合+装满可能性总数
          • 518. 零钱兑换 II(组合问题)
        • 1.3.2.3、重复选择+排列+装满可能性总数
          • 377. 组合总和 Ⅳ
          • 70. 爬楼梯
          • 139. 单词拆分
        • 1.3.2.4、重复选择 + 装满的最少数量
          • 322. 零钱兑换
          • 279. 完全平方数
      • 1.3.3、多重背包
      • 参考
    • 1.4、打家劫舍
      • 1.4.1、198. 打家劫舍
      • 1.4.2、213. 打家劫舍 II
      • 1.4.3、337.打家劫舍 III
    • 1.5、股票问题
      • 1.5.1、121. 买卖股票的最佳时机 - 只能买卖一次
      • 1.5.2、122. 买卖股票的最佳时机 II - 能买卖多次
      • 1.5.3、123. 买卖股票的最佳时机 III
      • 1.5.4、188. 买卖股票的最佳时机 IV
      • 1.5.5、309. 最佳买卖股票时机含冷冻期
      • 1.5.6、714. 买卖股票的最佳时机含手续费
    • 1.5、子序列问题
      • 1.5.1、子序列(不连续)
        • 1.5.1.1、300. 最长上升子序列
        • 1.5.1.2、1143. 最长公共子序列
        • 1.5.1.3、1035. 不相交的线
      • 1.5.2、子序列(连续)
        • 1.5.2.1、674. 最长连续递增序列
        • 1.5.2.2、718. 最长重复子数组
        • 1.5.2.3、53. 最大子序和
      • 1.5.3、编辑距离
        • 1.5.3.1、392. 判断子序列
        • 1.5.3.2、115. 不同的子序列
        • 1.5.3.3、583. 两个字符串的删除操作
        • 1.5.3.4、72. 编辑距离
      • 1.5.4、回文
        • 1.5.4.1、647. 回文子串
        • 1.5.4.2、最长回文子串
        • 1.5.4.3、516. 最长回文子序列

一、动态规划

动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。

动态规划中每一个状态一定是由上一个状态推导出来的。而贪心算法不同,贪心没有状态推导,而是从局部直接选最优的。

动态规划问题,将被拆解为如下五步:

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

1.1、基础问题1

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 nelse:return self.fib(n-1) + self.fib(n-2)

时间复杂度:O(2^n)
空间复杂度:O(n)

2高级解法:动态规划:

  1. 确定dp数组(dp table)以及下标的含义
    dp[i]的含义是:第i个数的斐波那契数值是dp[i]
  2. 确定递推公式
    题目已经给我们了,状态转移方程:F(n) = F(n - 1) + F(n - 2),其中 n > 1
  3. dp数组如何初始化
    题目也直接给了我们,F(0) = 0,F(1) = 1
  4. 确定遍历顺序
    从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的
  5. 举例推导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 ndp = [0]*(n+1)dp[0] = 0dp[1] = 1for 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 ndp1, p2 = 0, 1for i in range(2, n+1):dp1, dp2 = dp2, dp1+dp2return dp2

时间复杂度:O(n)
空间复杂度:O(1)


1.1.2、70. 爬楼梯

力扣题目链接

1一般解法:递归(超时):

class Solution:def climbStairs(self, n: int) -> int:if n<2:return nelse:return self.climbStairs(n-1) + self.climbStairs(n-2)

2高级解法:动态规划:

  1. 确定dp数组(dp table)以及下标的含义
    dp[i]的含义是:上第i个阶梯的步数为dp[i]
  2. 确定递推公式
    因为一次走一步或两步,dp[i-1]往上走一个台阶为dp[i],dp[i-2]往上走两个台阶为dp[i],那么dp[i] = dp[i-1]+dp[i-2]
  3. dp数组如何初始化
    这里比较有争议的点是,dp[0]是0还是1。按照公式递推,dp[0]应该是1,但是按照实际场景,0阶台阶走的步数应该是0。
    这里直接说明,不考虑dp[0]。如果初始化,只初始化dp[1] = 1,dp[2] = 2,然后从i = 3开始递推,这样才符合dp[i]的定义。
  4. 确定遍历顺序
    从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的
  5. 举例推导dp数组
    举例当n为5的时候,dp table(dp数组)应该是这样的:
    在这里插入图片描述
    可以看出这就是上一题的斐波那契数列,唯一的区别是,没有讨论dp[0]应该是什么,因为dp[0]在本题没有意义。

根据规则代码如下:

class Solution:def climbStairs(self, n: int) -> int:# n为1 和 2if n<3:return n# 初始化# 一阶楼梯dp1 = 1# 二阶楼梯dp2 = 2# 递归公式for i in range(3, n+1):dp1, dp2 = dp2, dp1+dp2return dp2

1.1.3、746. 使用最小花费爬楼梯

力扣题目链接

高级解法:动态规划:

  1. 确定dp数组以及下标的含义:
    动态规划需要有一个数组来记录状态,这里只用一个一维数组dp[i]就可以了。
    dp[i]的定义:到达第i个台阶所花费的最少体力为dp[i]。
  2. 确定递推公式:
    因为一次走一步或者两步,那么可以有两个途径得到dp[i],一个是dp[i-1] 一个是dp[i-2]。
    因为是求花费的体力最小,那么一定是选最小的,所以dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i],这个意思是,选取前一步或者两步的最小值,加上走到当前台阶的体力,等于从头开始走到当前的总体力花费。
  3. dp数组如何初始化
    同理,题目中由台阶0或1开始,那么初始化dp[0] 和dp[1]就够了,后面的由公式递推。
dp0, dp1 = cost[0], cost[1]
  1. 确定遍历顺序
    因为是模拟台阶,而且dp[i]又dp[i-1]dp[i-2]推出,所以是从前到后遍历cost数组就可以了。

  2. 举例推导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)

这里同样可以只维持两个值就行了,从而使代码:
时间复杂度:O(n)
空间复杂度:O(1)

但为了直观,平时还是建议上述第一种写法。


1.2、基础问题2

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.动态规划:

  1. 确定dp数组(dp table)以及下标的含义
    dp[i][j] :表示从(0, 0)出发,到(i, j) 有dp[i][j]条不同的路径。

  2. 确定递推公式
    要求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]只有这两个方向过来。

  3. 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
  1. 确定遍历顺序
    这里要看一下递归公式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]一定是有数值的。

  2. 举例推导dp数组
    如图所示:
    在这里插入图片描述
    综上,代码如下:

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]*nfor 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. 不同路径Ⅱ

力扣题目链接

动态规划:
动规五部曲:

  1. 确定dp数组(dp table)以及下标的含义
    dp[i][j] :表示从(0, 0)出发,到(i, j) 有dp[i][j]条不同的路径。
    这个同前面的题的含义
  2. 确定递推公式
    递推公式和 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];
  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:breakdp[0][i] = 1
for j in range(n):if obstacleGrid[j][0] == 1:breakdp[j][0] = 1

注意代码里for循环的终止条件,一旦遇到obstacleGrid[i][0] == 1的情况就停止dp[i][0]的赋值1的操作,dp[0][j]同理。

  1. 确定遍历顺序
    从递归公式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:continuedp[i][j] = dp[i-1][j] + dp[i][j-1]
  1. 举例推导dp数组
    拿示例1来举例如题:
    在这里插入图片描述
    对应的dp table 如图:
    在这里插入图片描述
    综上所述:
class Solution:def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:length = len(obstacleGrid)width = len(obstacleGrid[0])# 如果障碍物在起点或终点,直接返回0if obstacleGrid[0][0] == 1 or obstacleGrid[length-1][width-1]==1:return 0# 创建一个dp tabledp = [[0]*width for _ in range(length)]# 两个循环,初始化 dp tablefor i in range(length):# 遇到障碍物,后续都为0if obstacleGrid[i][0] == 1:breakdp[i][0] = 1for j in range(width):# 遇到障碍物,后续都为0if obstacleGrid[0][j] == 1:breakdp[0][j] = 1# 填表for i in range(1, length):for j in range(1, width):if obstacleGrid[i][j] == 1:continuedp[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])# 如果障碍物在起点或终点,直接返回0if obstacleGrid[0][0] == 1 or obstacleGrid[length-1][width-1]==1:return 0# 创建一个dp tabledp = [0]*width# 一个循环,先从obstacleGrid第一行开始for i in range(width):# 遇到障碍物,后续都为0if obstacleGrid[0][i] == 1:breakdp[i] = 1# 填表# 从第二行开始for i in range(1, length):# 这里不能从1开始,因为可能位置0有障碍物,所以从第0列开始for j in range(width):# 有障碍物,走法数量直接为0if 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、343. 整数拆分

力扣题目链接

1 动态规划:
这道题求整数拆分后的最大乘积值的组合,使用动态规划来解,大的整数的拆分最大组合乘积等于其子整数的拆分最大组合乘积的乘积。

  1. 确定dp数组(dp table)以及下标的含义
    dp[i]:拆分数字i,可以得到的最大乘积为dp[i]。
  2. 确定递推公式
    递推公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
    j * (i - j) 是单纯的把整数拆分为两个数相乘,而j * dp[i - j]是拆分成两个以及两个以上的个数相乘,加上dp[i],是跟上一次拆分组合比,每次保留最大值。
  3. dp的初始化
    有的题解里会给出dp[0] = 1,dp[1] = 1的初始化,但解释比较牵强,主要还是因为这么初始化可以把题目过了。严格从dp[i]的定义来说,dp[0] dp[1] 就不应该初始化,也就是没有意义的数值。
    拆分0和拆分1的最大乘积是多少?这是无解的。
    这里只初始化dp[2] = 1,从dp[i]的定义来说,拆分数字2,得到的最大乘积是1。
  4. 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]))
  1. 举例推导dp数组
    举例当n为10 的时候,dp数组里的数值,如下:
    在这里插入图片描述
class Solution:def integerBreak(self, n: int) -> int:# 数值加上一,索引才会到ndp = [0]*(n+1)# 初始化,从2开始,因为k>=2dp[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 1if n==3:return 2if n==4:return 4result = 1while n>4:result *= 3n -= 3result *= nreturn result

1.2.4、96. 不同的二叉搜索树

力扣题目链接

动态规划:

  1. 确定dp数组(dp table)以及下标的含义
    dp[i] : i个节点组成的二叉搜索树的个数为dp[i]。
  2. 确定递推公式
    在上面的分析中,其实已经看出其递推关系, dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量]
    j相当于是头结点的元素,从1遍历到i为止。
    所以递推公式:dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量
  3. dp数组如何初始化
    初始化,只需要初始化dp[0]就可以了,推导的基础,都是dp[0]。
    那么dp[0]应该是多少呢?
    从定义上来讲,空节点也是一棵二叉树,也是一棵二叉搜索树,这是可以说得通的。
    从递归公式上来讲,dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量] 中以j为头结点左子树节点数量为0,也需要dp[以j为头结点左子树节点数量] = 1, 否则乘法的结果就都变成0了。
    所以初始化dp[0] = 1
  4. 确定遍历顺序
    首先一定是遍历节点数,从递归公式: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]
  1. 举例推导dp数组
    n为5时候的dp数组状态如图:
    在这里插入图片描述
    综上,代码如下:
class Solution:def numTrees(self, n: int) -> int:# 从0到ndp = [0]*(n+1)dp[0] = 1for 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(n2)O(n^2)O(n2)
空间复杂度:O(n)O(n)O(n)



1.3、背包问题

在这里插入图片描述
背包问题,简单来说就是将单次、无限重复或者有限重复数量的物品,通过满足一定的题目需求,而放入有限大小的背包中。

而根据物品的三种不同规则,我们将分为三种背包问题:01背包,完全背包和多重背包。

它们的总结如下图:
在这里插入图片描述
但我们可以看到还有其他类型的背包在其中,但对于面试的话,其实掌握01背包,和完全背包,就够用了,最多可以再来一个多重背包。

至于其他的背包问题,面试几乎不会问,都是竞赛级别的了,leetcode上连多重背包的题目都没有,所以题库也告诉我们,01背包和完全背包就够用了。但如果你热爱学习,喜欢钻研,则一本背包问题的经典书籍送给你 背包问题九讲 2.0。

理解背包问题,01背包非常基础和重要,因为其他问题基本由此为基础衍生而来。我们先从纯背包问题开始,去理解和学习,再去延伸和转化到leetcode题目。



1.3.1、01背包

一次数量的物品,根据要求放入背包中。

暴力解法:
每一件物品其实只有两个状态:取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是o(2n)o(2^n)o(2n),这里的n表示物品数量。

所以暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!


1.3.1.1、单次选择+最大价值

问题描述:nnn件物品和一个最多能背重量为www的背包。第iii件物品的重量是weight[i]weight[i]weight[i],得到的价值是value[i]value[i]value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大

简单例子:
w=4w=4w=4重量的背包
n=3n=3n=3件物品,表示如下:

重量 weight[i]价值 value[i]
物品0115
物品1320
物品2430

问背包能背的物品的最大价值是多少?

(1). 二维DP数组

动态规划五部曲

  1. 确定dp数组以及下标的含义
    对于背包问题,有一种写法, 是使用二维数组,即dp[i][j]dp[i][j]dp[i][j] 表示从下标为[0−i][0-i][0i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
    在这里插入图片描述

  2. 确定递归公式
    dp[i][j]dp[i][j]dp[i][j]的含义为,从下标为0−i0-i0i的物品里任取,放进容量为j的背包,价值总和最大是多少。
    从两个方向可以推出dp[i][j]dp[i][j]dp[i][j]:

    • 不放物品i: 当物品i的重量大于背包j的重量时(即把背包清空也装不下),物品i无法放进背包中,所以被背包内的价值依然和前面相同,里面不放物品i的最大价值,此时dp[i][j]dp[i][j]dp[i][j]就是dp[i−1][j]dp[i - 1][j]dp[i1][j]
    • 放物品i: 想要放入一个物品,要此刻dp[i][j]dp[i][j]dp[i][j]的背包腾出物品i以及weight[i]weight[i]weight[i]的重量,于是腾出后的价值为dp[i−1][j−weight[i]]dp[i-1][j-weight[i]]dp[i1][jweight[i]]i−1i-1i1则为腾出i之后前面000 - i−1i-1i1的物品,j−weight[i]j-weight[i]jweight[i]为腾出weight[i]weight[i]weight[i]的重量。放入weight[i]weight[i]weight[i]重量的物品后的总价值为dp[i−1][j−weight[i]]+value[i]dp[i-1][j-weight[i]]+value[i]dp[i1][jweight[i]]+value[i]
  3. 数组的初始化
    首先从dp[i][j]dp[i][j]dp[i][j]的定义出发,如果背包容量jjj000的话,即dp[i][0]dp[i][0]dp[i][0],无论是选取哪些物品,背包价值总和一定为000。如图:
    在这里插入图片描述
    再看其他情况,状态转移方程 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])dp[i][j]=max(dp[i1][j],dp[i1][jweight[i]]+value[i]),因为iii是由i−1i-1i1推导而来,所以i=0i=0i=0一定要初始化。
    即初始化dp[0][j]dp[0][j]dp[0][j],存放编号为0的物品时,各个容量的背包所能存放的最大值。
    于是:

    • weight[0]>jweight[0]>jweight[0]>j,说明当前背包的容量装不下第0个物品,那么dp[0][j]=0dp[0][j]=0dp[0][j]=0
    • weight[0]<=jweight[0]<=jweight[0]<=j,说明当前的背包的重量能够装下第0个物品,那么dp[0][j]=value[0]dp[0][j]=value[0]dp[0][j]=value[0]
for j in range(bagweight):if j>=weight[0]:dp[0][j]=value[0]else:dp[0][j]=0

至于其他值,都会被递推公式推出结果,直接初始化微为即可,初始化完成后为:
在这里插入图片描述
4. 确定遍历顺序
有两个遍历的维度:物品与背包重量。
先遍历 物品还是先遍历背包重量呢?两者皆可,但是先遍历物品更好理解,比较贴合上面的图。

# 物品的编号
for i in range(1, len(weight)):# 包的大小for j in range(1, bagweight):# 大于包的大小,不放,与上一个物品价值相等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])
  1. 举例推导dp数组
    最终结果就是dp[2][4]dp[2][4]dp[2][4]
    在这里插入图片描述

整体代码(二维):

def bagpack_problem1(bag_size, weight, value) -> int: # rows 物品 cols 背包rows, cols = len(weight), bag_size + 1# 这里先遍历物品,再遍历背包。# 若先遍历背包,再遍历物品,下面dp表的创建行列要互换,初始化互换,下面的两个循环也要互换dp = [[0 for _ in range(cols)] for _ in range(rows)]# 初始化dp数组# 第0列,0-rows行,因为背包大小为0,则直接为0for i in range(rows): dp[i][0] = 0first_item_weight, first_item_value = weight[0], value[0]# 第0行,1-rows列(因为0在前面已经赋值为0了,所以从1开始),小于等于背包大小的物品赋予物品大小的值,其余为0for j in range(1, cols): 	if first_item_weight <= j: dp[0][j] = first_item_value# 更新dp数组: 先遍历物品, 再遍历背包. for i in range(1, len(weight)): cur_weight, cur_val = weight[i], value[i]# 注意这里能写成for j in range(nums[i],target+1)这样吗?# 不行,因为前面的值需要if cur_weight > j 去将0填空,便于下一行的值的依赖# 所以这里不同于二维数组,不需要依赖,而直接到nums[i]为止for j in range(1, cols): if cur_weight > j: # 说明背包装不下当前物品. dp[i][j] = dp[i - 1][j] # 所以不装当前物品. else: # 定义dp数组: dp[i][j] 前i个物品里,放进容量为j的背包,价值总和最大是多少。dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - cur_weight]+ cur_val)print(dp)if __name__ == "__main__": bag_size = 4weight = [1, 3, 4]value = [15, 20, 30]bagpack_problem1(bag_size, weight, value)

(2). 一维DP数组(滚动数组)

背包问题的状态其实都是可以压缩的。

在使用二维数组的时候,递推公式: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])dp[i][j]=max(dp[i1][j],dp[i1][jweight[i]]+value[i])

如果把dp[i−1]dp[i - 1]dp[i1]那一层拷贝到dp[i]dp[i]dp[i]上,表达式完全可以是:dp[i][j]=max(dp[i][j],dp[i][j−weight[i]]+value[i])dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i])dp[i][j]=max(dp[i][j],dp[i][jweight[i]]+value[i])

与其把dp[i−1]dp[i - 1]dp[i1]这一层拷贝到dp[i]dp[i]dp[i]上,不如只用一个一维数组了,只用dp[j]dp[j]dp[j](一维数组,也可以理解是一个滚动数组)。

这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。

动态规划五部曲

  1. 确定dpdpdp数组的定义
    在一维dpdpdp数组中,dp[j]dp[j]dp[j]表示:容量为jjj的背包,所背的物品价值可以最大为dp[j]dp[j]dp[j]
  2. 一维dpdpdp数组的递推公式
    dp[j]dp[j]dp[j]为 容量为jjj的背包所背的最大价值,那么如何推导dp[j]dp[j]dp[j]呢?
    dp[j]dp[j]dp[j]可以通过dp[j−weight[i]]dp[j - weight[i]]dp[jweight[i]]推导出来,dp[j−weight[i]]dp[j - weight[i]]dp[jweight[i]]表示容量为j−weight[i]j - weight[i]jweight[i]的背包所背的最大价值。
    dp[j−weight[i]]+value[i]dp[j - weight[i]] + value[i]dp[jweight[i]]+value[i] 表示 容量为 jjj - 物品iii重量 的背包 加上 物品iii的价值。(也就是容量为jjj的背包,放入物品iii了之后的最大总价值即:dp[j]dp[j]dp[j]
    此时dp[j]dp[j]dp[j]有两个选择,一个是取自己dp[j]dp[j]dp[j] 相当于 二维dp数组中的dp[i−1][j]dp[i-1][j]dp[i1][j],即不放物品iii,一个是取dp[j−weight[i]]+value[i]dp[j - weight[i]] + value[i]dp[jweight[i]]+value[i],即放物品iii,指定是取最大的,因为是求最大价值,所以递推公式为:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
  1. 一维dp数组如何初始化
    那么dp数组除了下标0的位置,初始为0,其他下标应该初始化多少呢?
    看一下递归公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。
    这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。
    那么我假设物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。
  2. 一维dp数组遍历顺序
for i in range(len(weight)):for j in reversed(range(weight[i], bagweight+1)):dp[j] = max(dp[j], dp[j-weight[i]]+value[i])

这里大家发现和二维dp的写法中,遍历背包的顺序是不一样的!
二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。
为什么呢?
倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!
举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15
如果正序遍历
dp[1] = dp[1 - weight[0]] + value[0] = 15
dp[2] = dp[2 - weight[0]] + value[0] = 30
此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。
为什么倒序遍历,就可以保证物品只放入一次呢?
倒序就是先算dp[2]
dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)
dp[1] = dp[1 - weight[0]] + value[0] = 15
所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
那么问题又来了,为什么二维dp数组历的时候不用倒序呢?
因为对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖!
再来看看两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?不可以!
因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。
倒序遍历的原因是,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。
(这里如果读不懂,就再回想一下dp[j]的定义,或者就把两个for循环顺序颠倒一下试试!)
所以一维dp数组的背包在遍历顺序上和二维其实是有很大差异的!,这一点大家一定要注意。

  1. 举例推导dp数组
    一维dp,分别用物品0,物品1,物品2 来遍历背包,最终得到结果如下:
    在这里插入图片描述

整体代码(一维):

def bag_problem():weight = [1, 3, 4]value = [15, 20, 30]bag_weight = 4# 初始化: 全为0dp = [0] * (bag_weight + 1)# 先遍历物品, 再遍历背包容量for i in range(len(weight)):for j in range(bag_weight, weight[i] - 1, -1):# 递归公式dp[j] = max(dp[j], dp[j - weight[i]] + value[i])print(dp)bag_problem()

总结:
动态规划问题枚举的顺序只有一个原则:如果b状态的计算依赖于a状态,那么a状态必须在b状态之前被枚举和计算。

二维数组时,物品和背包的顺序,从小到大遍历,二者可以内外互换,物品顺序可颠倒,但初始化操作也要相应颠倒;而背包只可逆序,因为背包的结果与前面一次较小的背包结果有关,而前面的结果是上一层的拷贝,而不会受到这一层更新的干扰而重复累加,避免重复计算,重复计算会变成完全背包。

对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,所以二维无需逆序遍历,当然背包也可以逆序,不影响结果。

01背包最大重量/价值二维一维
初始化第0行0列初始化无需初始化
内外循序物品+背包 | 背包+物品物品+背包
物品顺序正序|逆序正序|逆序
背包顺序正序|逆序逆序
递推公式dp[i][j]=max(dp[i-1]j[j], dp[i-1][j-weight[i]]+value[i])dp[j]=max(dp[j], dp[j-weight[i]]+value[i])

1.3.1.2、单次选择+最大重量

问题描述:nnn件物品和一个最多能背重量为www的背包。第i件物品的重量是weight[i]weight[i]weight[i],每件物品只能用一次,求解将哪些物品装入背包里物品重量总和最大

分析:
这个问题没有给每个物品的价值,也没有问最多能获得多少价值,而是问最多能装多满。
实际上在这里,如果我们把每个物品的大小当作每个物品的价值,就可以完美解决这个问题。

简单例子:
w=4w=4w=4重量的背包
n=3n=3n=3件物品,表示如下:
这里增设一列不存在的价值,值与重量相等,模拟等于上一题求最大价值。

重量 weight[i]价值 value[i]
物品011
物品133
物品244

问背包能背的物品的最大价值(或者重量)是多少?

代码同上,只不过values与weights相等,那么只需把values改成weight即可。

整体代码(一维,滚动数组):

def bag_problem():weight = [1, 3, 4]value = [15, 20, 30]bag_weight = 4# 初始化: 全为0dp = [0] * (bag_weight + 1)# 先遍历物品, 再遍历背包容量for i in range(len(weight)):for j in range(bag_weight, weight[i] - 1, -1):# 递归公式,这里注意不是+value[i]而是weight[i]dp[j] = max(dp[j], dp[j - weight[i]] + weight[i])print(dp)bag_problem()

416. 分割等和子集

416. 分割等和子集
解题思路: 等分,先判断总和能否被2整除,能否进行分隔,不能直接False,能的话继续判断里面的数,分隔后能否分成相等的两部分,等分必然是总和值/2的值的大小的背包装满,能装满则说明能等分,不能装满则False。

二维数组:

class Solution:def canPartition(self, nums: List[int]) -> bool:# target既是背包大小也是最大价值,问是否能装满target大小的背包target = sum(nums)# 如果是奇数则不能均分,直接False,如果偶数则再后续加以判断if target%2==1:return Falseelse:target //=2dp = [[0]*(target+1) for _ in range(len(nums))]# 遍历物品for i in range(len(nums)):# 遍历背包大小for j in range(target+1):# 背包小鱼物品大小if j<nums[i]:dp[i][j] = dp[i-1][j]# 能放但不放,或者放进去那个大取哪个else:dp[i][j] = max(dp[i-1][j], dp[i-1][j-nums[i]]+nums[i])return target == dp[-1][-1]

一维数组(滚动数组):

class Solution:def canPartition(self, nums: List[int]) -> bool:# target既是背包大小也是最大价值,问是否能装满target大小的背包target = sum(nums)# 如果是奇数则不能均分,直接False,如果偶数则再后续加以判断if target%2==1:return Falseelse:target //=2dp = [0]*(target+1)# 遍历物品for i in range(len(nums)):# 遍历背包大小,逆序防止累加# for j in reversed(range(nums[i], target+1)):for j in range(target, nums[i]-1, -1):dp[j] = max(dp[j], dp[j-nums[i]]+nums[i])return target == dp[-1]

1049. 最后一块石头的重量 II

1049. 最后一块石头的重量 II
该题与上一题很类似,隐藏的背包问题。最终的思路是将这个石头集合分成两部分,他们越相近,即离和中值越近,相差就越小。如果无法等分,那么因为这个中值为向下取整,所以离偏小的部分近一些,离偏大的部分远一些。那么以中值为背包,装满得到较小值部分的值,求石头的总体和,减去较小值部分,就为最大值部分,再减去一次较小值部分,得出最小的相差。

为什么不求较大值呢,因为较小值可以用中值找到最大背包,而较大值的最大背包找不到。

一维滚动数组:

class Solution:def lastStoneWeightII(self, stones: List[int]) -> int:su_m = sum(stones)target = su_m//2dp = [0]*(target+1)for i in range(len(stones)):for j in range(target, stones[i]-1,-1):dp[j] = max(dp[j], dp[j-stones[i]]+stones[i])return su_m-dp[-1]-dp[-1]

474. 一和零(双维度背包)

474. 一和零
二维背包问题,分别装0和1的数量的背包,两个数量用一个二维来存储。

class Solution:def findMaxForm(self, strs: List[str], m: int, n: int) -> int:dp = [[0]*(n+1) for _ in range(m+1)]# 遍历物品for s in strs:ones = s.count('1')zeros = s.count('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[-1][-1]

1.3.1.3、单次选择+装满可能性总数

问题描述:nnn件物品和一个最多能背重量为www的背包。第i件物品的重量是weight[i]weight[i]weight[i],每件物品只能用一次,求解装满背包有哪些不同的方法。

分析:
这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。

本题则是装满有几种方法。其实这就是一个组合问题了。求组合类问题的公式,都是类似这种:
1. 确定dp[j]的含义:
dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法

2. 递推公式:
只要获得nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。

例如:dp[j],j 为5,

如果那么
已经有一个1(nums[i]) 的话有 dp[4]种方法 凑成 容量为5的背包
已经有一个2(nums[i]) 的话有 dp[3]种方法 凑成 容量为5的背包
已经有一个3(nums[i]) 的话有 dp[2]中方法 凑成 容量为5的背包
已经有一个4(nums[i]) 的话有 dp[1]中方法 凑成 容量为5的背包
已经有一个5 (nums[i])的话有 dp[0]中方法 凑成 容量为5的背包

那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。

dp[j] += dp[j - nums[i]]

这个公式在后面在讲解背包解决排列组合问题的时候还会用到。

3. 初始化:
dp数组如何初始化?
从递推公式可以看出,在初始化的时候dp[0] 一定要初始化为1,因为dp[0]是在公式中一切递推结果的起源,如果dp[0]是0的话,递推结果将都是0。

4. 遍历顺序:
01背包问题一维dp的遍历,nums放在外循环,target在内循环,且内循环倒序。

简单例子:
w=4w=4w=4重量的背包
n=3n=3n=3件物品,表示如下:

重量 weight[i]
物品02
物品12
物品24

问有多少种装满背包的方法?
结果:[2, 2], [4]


494. 目标和

494. 目标和
将该题转化为背包装满的装法次数。
使用推导公式:

dp[j] += dp[j-nums[i]]

背包大小根据公式推导而来

left+right = sum
left-right = target
left = (target+sum)/2

初始化时,需要赋值第一个值为1,其余为0不变

dp[0]=1

举例推导:
输入:nums: [1, 1, 1, 1, 1], S: 3

bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4

dp数组状态变化如下:
在这里插入图片描述

代码整体如下:

class Solution:def findTargetSumWays(self, nums: List[int], target: int) -> int:# left+right = sum# left-right = target# left = (target+sum)/2s_um = sum(nums)# 目标值比所有值求和大,或者目标值不可分,那么方法为0if abs(target)>s_um or (target+s_um)%2:return 0# 背包大小,根据上面公式推导bagsize = (target+s_um)//2# 创建dp表dp = [0]*(bagsize+1)# 初始化第一个元素为1dp[0]=1# 同前面的二重循环for i in range(len(nums)):for j in range(bagsize, nums[i]-1, -1):# 递推公式,用来求背包装满的次数dp[j] += dp[j-nums[i]]return dp[-1]

注意:
有道题 39. 组合总和 大部分是使用回溯算法来做的,为什么不选用动态规划呢?因为题目所求的结果不同:

求个数列出组合
动态规划回溯爆搜

总结:

01背包最大重量/价值01背包最大重量/价值01背包可能性总数
维度二维一维一维
初始化第0行和第0列分别初始化无需初始化初始化dp[0]=1
内外循序物品+背包 | 背包+物品物品+背包物品+背包
物品顺序正序|逆序正序|逆序正序|逆序
背包顺序正序|逆序逆序逆序
递推公式dp[i][j]=max(dp[i-1]j[j], dp[i-1][j-weight[i]]+value[i])dp[j]=max(dp[j], dp[j-weight[i]]+value[i])dp[j]+=dp[j-nums[i]]

1.3.2、完全背包

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。


1.3.2.1、重复选择+最大价值

问题描述: 有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

简单例子:
w=4w=4w=4重量的背包
n=∞n=\inftyn=件物品,表示如下:

重量 weight[i]价值 value[i]
物品0115
物品1320
物品2430

问背包能背的物品的最大价值是多少?

对比下:
01背包的核心代码如下:

# 先遍历物品,再遍历背包
# 遍历物品
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])

我们知道01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。

而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即:

# 先遍历物品,再遍历背包
# 遍历物品
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])

在这里插入图片描述
01背包中二维dp数组的两个for遍历的先后循序是可以颠倒了,一维dp数组的两个for循环先后循序一定是先遍历物品,再遍历背包容量。

在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的

因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。

遍历物品在外层循环,遍历背包容量在内层循环,状态如图:
在这里插入图片描述
遍历背包容量在外层循环,遍历物品在内层循环,状态如图:
在这里插入图片描述
看了这两个图,大家就会理解,完全背包中,两个for循环的先后循序,都不影响计算dp[j]所需要的值(这个值就是下标j之前所对应的dp[j])。

先遍历背包再遍历物品,代码如下:

# 先遍历背包,再遍历物品
# 遍历背包
for j in range(bagWeight+1):# 遍历物品for i in range(len(weight)):if j-weight[i]>=0:dp[j] = max(dp[j-weight[i]]+value[i])

整体代码:

# 先遍历物品,再遍历背包
def bag_pack1():weight = [1, 3, 4]value = [15, 20, 30]bag_weight = 4dp = [0]*(bag_weight + 1)for i in range(len(weight)):for j in range(weight[i], bag_weight + 1):dp[j] = max(dp[j], dp[j - weight[i]] + value[i])print(dp[bag_weight])# 先遍历背包,再遍历物品
def bag_pack2():weight = [1, 3, 4]value = [15, 20, 30]bag_weight = 4dp = [0]*(bag_weight + 1)for j in range(bag_weight + 1):for i in range(len(weight)):if j >= weight[i]: dp[j] = max(dp[j], dp[j - weight[i]] + value[i])print(dp[bag_weight])if __name__ == '__main__':bag_pack1()bag_pack2()

总结:
因为是完全背包,物品可叠加,那么一维数组顺序遍历即可:

01背包最大重量/价值01背包最大重量/价值01背包可能性总数完全背包最大重量/价值
维度二维一维一维一维
初始化第0行和第0列分别初始化无需初始化初始化dp[0]=1无需初始化
内外循序物品+背包 | 背包+物品物品+背包物品+背包物品+背包|背包+物品
物品顺序正序|逆序正序|逆序正序|逆序正序|逆序
背包顺序正序|逆序逆序(避免重复)逆序正序(为了重复)
递推公式dp[i][j]=max(dp[i-1]j[j], dp[i-1][j-weight[i]]+value[i])dp[j]=max(dp[j], dp[j-weight[i]]+value[i])dp[j]+=dp[j-nums[i]]dp[i][j]=max(dp[i-1]j[j], dp[i-1][j-weight[i]]+value[i])

1.3.2.2、重复选择+组合+装满可能性总数

518. 零钱兑换 II(组合问题)

518. 零钱兑换 II

递推公式:
求背包装满的次数,根据公式:dp[j] = dp[j-nums[i]]

遍历顺序:
因为纯完全背包求得装满背包的最大价值是多少,和凑成总和的元素有没有顺序没关系,即:有顺序也行,没有顺序也行。所以纯完全背包是能凑成总和就行,不用管怎么凑的。

而本题要求凑成总和的组合数,元素之间明确要求没有顺序,那么每个方案个数是为组合数。

那么本题,两个for循环的先后顺序可就有说法了。

结果是求组合数,那么遍历顺序为:
先遍历物品,后遍历背包:

# 遍历物品
for i in range(len(coins)):# 遍历背包容量for j in range(coins[i], amount+1):dp[j] += dp[j-coins[i]]

如果是求排列数,那么遍历顺序为:

# 遍历背包容量
for j in range(amount+1):# 遍历物品for i in range(len(coins)):if j-coins[i]>=0:dp[j]+=dp[j-coins[i]]

整体代码:

class Solution:def change(self, amount: int, coins: List[int]) -> int:dp = [0]*(amount+1)dp[0]=1for i in range(len(coins)):for j in range(coins[i], amount+1):dp[j]+=dp[j-coins[i]]return dp[-1]

总结:
通过该例子,我们知道,在完全背包问题的装满可能性总数中,物品和背包的顺序在排列和组合问题中是有影响的:

排列组合
背包+物品物品+背包

综上:

01背包最大重量/价值01背包最大重量/价值01背包可能性总数完全背包最大重量/价值完全背包可能性总数
维度二维一维一维一维一维
初始化第0行和第0列分别初始化无需初始化初始化dp[0]=1无需初始化初始化dp[0]=1
内外循序物品+背包 | 背包+物品物品+背包物品+背包物品+背包|背包+物品排列: 背包+物品|组合: 物品+背包
物品顺序正序|逆序正序|逆序正序|逆序正序|逆序正序|逆序
背包顺序正序|逆序逆序(避免重复)逆序正序(为了重复)正序
递推公式dp[i][j]=max(dp[i-1]j[j], dp[i-1][j-weight[i]]+value[i])dp[j]=max(dp[j], dp[j-weight[i]]+value[i])dp[j]+=dp[j-nums[i]]dp[i][j]=max(dp[i-1]j[j], dp[i-1][j-weight[i]]+value[i])dp[j]+=dp[j-nums[i]]

1.3.2.3、重复选择+排列+装满可能性总数

377. 组合总和 Ⅳ

377. 组合总和 Ⅳ
完全背包,两个循环顺序执行
求次数,dp[j] = dp[j-nums[i]],并且dp[0]=1
求排列,先背包后物品。

根据上一题的规律,可以很轻易的做出此题:

class Solution:def combinationSum4(self, nums: List[int], target: int) -> int:dp = [0]*(target+1)dp[0]=1for j in range(target+1):for i in nums:if j-i>=0:dp[j]+=dp[j-i]return dp[-1]

70. 爬楼梯

70. 爬楼梯
同上题的思路,这是背包里求排列问题,即:1、2 步 和 2、1 步都是上三个台阶,但是这两种方法不一样。所以需将target放在外循环,将nums放在内循环。每一步可以走多次,这是完全背包,内循环需要从前向后遍历。

class Solution:def climbStairs(self, n: int) -> int:dp = [0]*(n+1)dp[0]=1# 遍历背包数,楼梯总长度for j in range(n+1):# 遍历物品,台阶数,1或2 for i in [1,2]:if j-i>=0:dp[j]+=dp[j-i]return dp[-1]

139. 单词拆分

139. 单词拆分

1.dp数组含义
dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。

2.递推公式
如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。

所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。

3.dp数组的初始化
从递推公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递推的根基,dp[0]一定要为true,否则递推下去后面都都是false了。

下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。

4.遍历顺序
题目中说是拆分为一个或多个在字典中出现的单词,所以这是完全背包。
“apple”, “pen” 是物品,那么我们要求 物品的组合一定是 “apple” + “pen” + “apple” 才能组成 “applepenapple”。

“apple” + “apple” + “pen” 或者 “pen” + “apple” + “apple” 是不可以的,那么我们就是强调物品之间顺序。

所以说,本题一定是 先遍历 背包,再遍历物品。所以是排列问题。

5.举例推导dp[i]
以输入: s = “leetcode”, wordDict = [“leet”, “code”]为例,dp状态如图:
在这里插入图片描述

所以整体代码如下:

class Solution:def wordBreak(self, s: str, wordDict: List[str]) -> bool:dp = [False]*(len(s)+1)dp[0]=True# 排列问题,先背包for j in range(1, len(s)+1):# 后物品,即单词for i in wordDict:if j>=len(i):dp[j] = dp[j] or dp[j-len(i)] and s[j-len(i):j]==ireturn dp[-1]

1.3.2.4、重复选择 + 装满的最少数量

322. 零钱兑换

322. 零钱兑换

1.dp数组的含义:
dp[j]:凑足总额为j所需钱币的最少个数为dp[j]

2.递推公式:
凑足总额为j - coins[i]的最少个数为dp[j - coins[i]],那么只需要加上一个钱币coins[i]即dp[j - coins[i]] + 1就是dp[j](考虑coins[i])所以dp[j] 要取所有 dp[j - coins[i]] + 1 中最小的。

求最小次数:dp[j] = min(dp[j], dp[j - coin[i]] + 1)

3.数组初始化
dp数组如何初始化
首先凑足总金额为0所需钱币的个数一定是0,那么dp[0] = 0;

其他下标对应的数值呢?考虑到递推公式的特性,如果默认初始化为0,那么每次求min的时候,都会取默认值0而不是真实最小值,那么默认0就不可取。所以dp[j]必须初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖。所以下标非0的元素都是应该是最大值。

代码如下:

dp = [float('inf')]*(amount+1)
dp[0] = 1

4.遍历顺序
求所需钱币最小个数,所以并不讲究物品和背包的先后顺序,因为既不是排列也不是组合。

5.举例推导dp数组
以输入:coins = [1, 2, 5], amount = 5为例:
在这里插入图片描述
整理代码如下:

class Solution:def coinChange(self, coins: List[int], amount: int) -> int:'''版本一'''# 初始化dp = [float("inf")]*(amount + 1)dp[0] = 0# 遍历物品for coin in coins:# 遍历背包for j in range(coin, amount + 1):dp[j] = min(dp[j], dp[j - coin] + 1)return dp[amount] if dp[amount] != float("inf") else -1def coinChange1(self, coins: List[int], amount: int) -> int:'''版本二'''# 初始化dp = [float("inf")]*(amount + 1)dp[0] = 0# 遍历物品for j in range(1, amount + 1):# 遍历背包for coin in coins:if j >= coin:dp[j] = min(dp[j], dp[j - coin] + 1)return dp[amount] if dp[amount] != float("inf") else -1

总结:

01背包最大重量/价值01背包最大重量/价值01背包可能性总数完全背包最大重量/价值完全背包可能性总数完全背包最少数量
维度二维一维一维一维一维一维
初始化第0行和第0列分别初始化无需初始化初始化dp[0]=1无需初始化初始化dp[0]=1dp = [float(“inf”)]*(amount + 1) dp[0] = 0
内外循序物品+背包 | 背包+物品物品+背包物品+背包物品+背包|背包+物品排列: 背包+物品|组合: 物品+背包背包+物品|物品+背包
物品顺序正序|逆序正序|逆序正序|逆序正序|逆序正序|逆序正序
背包顺序正序|逆序逆序(避免重复)逆序正序(为了重复)正序正序
递推公式dp[i][j]=max(dp[i-1]j[j], dp[i-1][j-weight[i]]+value[i])dp[j]=max(dp[j], dp[j-weight[i]]+value[i])dp[j]+=dp[j-nums[i]]dp[i][j]=max(dp[i-1]j[j], dp[i-1][j-weight[i]]+value[i])dp[j]+=dp[j-nums[i]]dp[j] = min(dp[j], dp[j - coin[i]] + 1)

279. 完全平方数

279. 完全平方数

这题同上一题零钱兑换,思路类似,不过得自创平方数集合作为物品。

class Solution:def numSquares(self, n: int) -> int:nums = [i*i for i in range(1, n+1) if i*i<=n]dp = [float('inf')]*(n+1)dp[0]=0# 先遍历物品,再遍历背包for i in nums:for j in range(i, n+1):dp[j] = min(dp[j], dp[j-i]+1)return dp[-1]

也可以先遍历背包,再遍历物品

# 遍历背包for j in range(1, n + 1):# 遍历物品for num in nums:if j >= num:dp[j] = min(dp[j], dp[j - num] + 1)

1.3.3、多重背包

这个问题与 0-1 背包的区别在于,0-1 背包中每种物品有且只有一个,而多重背包中一种物品有nums[i] 个。

对于多重背包,我在力扣上还没发现对应的题目,所以这里就做一下简单介绍,大家大概了解一下。

有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。

多重背包和01背包是非常像的, 为什么和01背包像呢?

每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。

例如:

背包最大重量为10。

物品为:

重量 weight[i]价值 value[i]数量
物品01152
物品13203
物品24302

问背包能背的物品最大价值是多少?

和如下情况有区别么?

重量 weight[i]价值 value[i]数量
物品01151
物品01151
物品13201
物品13201
物品13201
物品24301
物品24301
毫无区别,这就转成了一个01背包问题了,且每个物品只用一次。

这种方式来实现多重背包的代码如下:

def test_multi_pack1():'''版本一:改变物品数量为01背包格式'''weight = [1, 3, 4]value = [15, 20, 30]nums = [2, 3, 2]bag_weight = 10for i in range(len(nums)):# 将物品展开数量为1while nums[i] > 1:weight.append(weight[i])value.append(value[i])nums[i] -= 1dp = [0]*(bag_weight + 1)# 遍历物品for i in range(len(weight)):# 遍历背包for j in range(bag_weight, weight[i] - 1, -1):dp[j] = max(dp[j], dp[j - weight[i]] + value[i])print(" ".join(map(str, dp))if __name__ == '__main__':test_multi_pack1()

时间复杂度:O(m × n × k),m:物品种类个数,n背包容量,k单类物品数量
也有另一种实现方式,就是把每种商品遍历的个数放在01背包里面在遍历一遍。

代码如下:(详看注释)

def test_multi_pack2():'''版本:改变遍历个数'''weight = [1, 3, 4]value = [15, 20, 30]nums = [2, 3, 2]bag_weight = 10dp = [0]*(bag_weight + 1)for i in range(len(weight)):for j in range(bag_weight, weight[i] - 1, -1):# 以上是01背包,加上遍历个数for k in range(1, nums[i] + 1):if j - k*weight[i] >= 0:dp[j] = max(dp[j], dp[j - k*weight[i]] + k*value[i])print(" ".join(map(str, dp)))if __name__ == '__main__':test_multi_pack2()

时间复杂度:O(m × n × k),m:物品种类个数,n背包容量,k单类物品数量
从代码里可以看出是01背包里面在加一个for循环遍历一个每种商品的数量。 和01背包还是如出一辙的。

当然还有那种二进制优化的方法,其实就是把每种物品的数量,打包成一个个独立的包。

和以上在循环遍历上有所不同,因为是分拆为各个包最后可以组成一个完整背包,具体原理我就不做过多解释了,大家了解一下就行,面试的话基本不会考完这个深度了,感兴趣可以自己深入研究一波。

总结:
多重背包在面试中基本不会出现,力扣上也没有对应的题目,大家对多重背包的掌握程度知道它是一种01背包,并能在01背包的基础上写出对应代码就可以了。

至于背包九讲里面还有混合背包,二维费用背包,分组背包等等这些,大家感兴趣可以自己去学习学习,这里也不做介绍了,面试也不会考。


参考

Algorithm and Data Structure Notes
LeetCode BackPack 背包问题
代码随想录
随想录视频



1.4、打家劫舍

1.4.1、198. 打家劫舍

198. 打家劫舍
每个家庭有不同金钱数量,盗窃的家庭不能相邻,求最大盗窃金额。

class Solution:def rob(self, nums: List[int]) -> int:if len(nums)==1:return nums[-1]dp = [0]*len(nums)dp[0] = nums[0]dp[1] = max(nums[0], nums[1])for i in range(2, len(nums)):dp[i] = max(dp[i-2]+nums[i], dp[i-1])return dp[-1]

1.4.2、213. 打家劫舍 II

213. 打家劫舍 II
承接上题,但是房子是环状,即首位相连的。

那么要考虑第一个房子和最后一个房子会相临,只能取其一。

所以,我们把房子变成两种情况:

  1. 不考虑第一个房子 nums[1:]
  2. 不考虑最后一个房子nums[:-1]

以上两种情况取最大值后再去他们两个之间的最大值,可以通过上题的解法来解了:

class Solution:def rob(self, nums: List[int]) -> int:if len(nums)==1:return nums[-1]def roblist(nums):if len(nums)==1:return nums[-1]dp = [0]*len(nums)dp[0]=nums[0]dp[1]=max(nums[0], nums[1])for i in range(2, len(nums)):dp[i] = max(dp[i-1], dp[i-2]+nums[i])return dp[-1]return max(roblist(nums[1:]), roblist(nums[:-1]))

1.4.3、337.打家劫舍 III

337.打家劫舍 III
题目思路类似第一种类型,但是需要掌握二叉树的遍历。

class Solution:def rob(self, root: Optional[TreeNode]) -> int:# dp数组(dp table)以及下标的含义:# 1. 下标为 0 记录 **不偷该节点** 所得到的的最大金钱# 2. 下标为 1 记录 **偷该节点** 所得到的的最大金钱dp = self.traversal(root)return max(dp)# 要用后序遍历, 因为要通过递归函数的返回值来做下一步计算def traversal(self, node):# 递归终止条件,就是遇到了空节点,那肯定是不偷的if not node:return (0, 0)left = self.traversal(node.left)right = self.traversal(node.right)# 不偷当前节点, 偷子节点val_0 = max(left[0], left[1]) + max(right[0], right[1])# 偷当前节点, 不偷子节点val_1 = node.val + left[0] + right[0]return (val_0, val_1)


1.5、股票问题

像这样的股票问题,使用动态规划,创建两列即两个状态的二维数组,照常初始化,但需要根据题意去得出特定的递推公式,便可完成该题。


1.5.1、121. 买卖股票的最佳时机 - 只能买卖一次

121. 买卖股票的最佳时机

贪心算法:

class Solution:def maxProfit(self, prices: List[int]) -> int:low = float('inf')result = 0# 遍历,每一个值当做最大值,其中可能存在最小值,进行替换# 右边当最大值,在其左边找最小值for i in range(len(prices)):low = min(low, prices[i])result = max(result, prices[i]-low)return result

递推公式,两种状态:
要么持有该股票:那么要么之前就买了dp[i-1][0],保持原样,要么刚买-prices[i]
要么不持有该股票:那么之前就没买dp[i-1][1],也保持原样,要么刚卖prices[i]+dp[i-1],取差值。

动态规划:

class Solution:def maxProfit(self, prices: List[int]) -> int:# 创建二维数组,两个状态,持有和不持有length = len(prices)if length==0:return 0dp = [[0, 0] for _ in range(length)]# 初始化,第一天持有则买入,第一天不持有则仍然为0dp[0][0] = -prices[0]dp[0][1] = 0# 开始遍历for i in range(1, length):# 持有股票dp[i][0] = max(dp[i-1][0], -prices[i])# 不持有股票dp[i][1] = max(dp[i-1][1], prices[i]+dp[i-1][0])return dp[-1][1]

滚动数组:

class Solution:def maxProfit(self, prices: List[int]) -> int:length = len(prices)dp = [[0] * 2 for _ in range(2)] #注意这里只开辟了一个2 * 2大小的二维数组dp[0][0] = -prices[0]dp[0][1] = 0for i in range(1, length):dp[i % 2][0] = max(dp[(i-1) % 2][0], -prices[i])dp[i % 2][1] = max(dp[(i-1) % 2][1], prices[i] + dp[(i-1) % 2][0])return dp[(length-1) % 2][1]

进一步改进:

class Solution:def maxProfit(self, prices: List[int]) -> int:length = len(prices)dp0, dp1 = -prices[0], 0 #注意这里只维护两个常量,因为dp0的更新不受dp1的影响for i in range(1, length):dp1 = max(dp1, dp0 + prices[i])dp0 = max(dp0, -prices[i])return dp1

1.5.2、122. 买卖股票的最佳时机 II - 能买卖多次

122. 买卖股票的最佳时机 II
这里重申一下dp数组的含义:

dp[i][0] 表示第i天持有股票所得现金。
dp[i][1] 表示第i天不持有股票所得最多现金

如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来:

  • 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
  • 第i天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即:dp[i - 1][1] - prices[i]
    注意这里和121. 买卖股票的最佳时机 唯一不同的地方,就是推导dp[i][0]的时候,第i天买入股票的情况。

在121. 买卖股票的最佳时机 中,因为股票全程只能买卖一次,所以如果买入股票,那么第i天持有股票即dp[i][0]一定就是 -prices[i]。

而本题,因为一只股票可以买卖多次,所以当第i天买入股票的时候,所持有的现金可能有之前买卖过的利润。那么第i天持有股票即dp[i][0],如果是第i天买入股票,所得现金就是昨天不持有股票的所得现金 减去 今天的股票价格 即:dp[i - 1][1] - prices[i]。

再来看看如果第i天不持有股票即dp[i][1]的情况, 依然可以由两个状态推出来

  • 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
  • 第i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金即:prices[i] + dp[i - 1][0]
class Solution:def maxProfit(self, prices: List[int]) -> int:# 步骤同上一题dp = [[0,0] for _ in range(len(prices))]dp[0][0] = -prices[0]dp[0][1] = 0for i in range(1, len(prices)):#注意这里是和121. 买卖股票的最佳时机唯一不同的地方dp[i][0] = max(dp[i-1][0], dp[i-1][1]-prices[i])dp[i][1] = max(dp[i-1][1], dp[i-1][0]+prices[i])return dp[-1][1]

滚动数组:

class Solution:def maxProfit(self, prices: List[int]) -> int:length = len(prices)dp = [[0] * 2 for _ in range(2)] #注意这里只开辟了一个2 * 2大小的二维数组dp[0][0] = -prices[0]dp[0][1] = 0for i in range(1, length):dp[i % 2][0] = max(dp[(i-1) % 2][0], dp[(i-1) % 2][1] - prices[i])dp[i % 2][1] = max(dp[(i-1) % 2][1], dp[(i-1) % 2][0] + prices[i])return dp[(length-1) % 2][1]

其他解法:

class Solution:def maxProfit(self, prices: List[int]) -> int:# 创建dp table# 含义第N天的最大利润dp = [0] * (len(prices)+1)# 初始化,第0天和第一天没有利润,为0,当然这里多余# dp[0] = dp[1] = 0# 从第二天开始产生正负利润for i in range(2, len(prices)+1):# 当前第N天的利润等于,max(之前的利润,今天的利润+之前的利润)# 这里取最大值,若今天产生负利润,则不买,只取之前的利润dp[i] = max(dp[i-1], prices[i-1]-prices[i-2]+dp[i-1])return dp[-1]

1.5.3、123. 买卖股票的最佳时机 III

123. 买卖股票的最佳时机 III

class Solution:def maxProfit(self, prices: List[int]) -> int:if len(prices) == 0:return 0dp = [[0] * 5 for _ in range(len(prices))]dp[0][1] = -prices[0]dp[0][3] = -prices[0]for i in range(1, len(prices)):dp[i][0] = dp[i-1][0]dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])dp[i][2] = max(dp[i-1][2], dp[i-1][1] + prices[i])dp[i][3] = max(dp[i-1][3], dp[i-1][2] - prices[i])dp[i][4] = max(dp[i-1][4], dp[i-1][3] + prices[i])return dp[-1][4]

官方降低复杂度解法:

class Solution:def maxProfit(self, prices: List[int]) -> int:if len(prices) == 0:return 0dp = [0] * 5 dp[1] = -prices[0]dp[3] = -prices[0]for i in range(1, len(prices)):dp[1] = max(dp[1], dp[0] - prices[i])dp[2] = max(dp[2], dp[1] + prices[i])dp[3] = max(dp[3], dp[2] - prices[i])dp[4] = max(dp[4], dp[3] + prices[i])return dp[4]

1.5.4、188. 买卖股票的最佳时机 IV

188. 买卖股票的最佳时机 IV

class Solution:def maxProfit(self, k: int, prices: List[int]) -> int:if len(prices) == 0:return 0dp = [[0] * (2*k+1) for _ in range(len(prices))]for j in range(1, 2*k, 2):dp[0][j] = -prices[0]for i in range(1, len(prices)):for j in range(0, 2*k-1, 2):dp[i][j+1] = max(dp[i-1][j+1], dp[i-1][j] - prices[i])dp[i][j+2] = max(dp[i-1][j+2], dp[i-1][j+1] + prices[i])return dp[-1][2*k]

滚动数组:

class Solution:def maxProfit(self, k: int, prices: List[int]) -> int:if len(prices) == 0: return 0dp = [0] * (2*k + 1)for i in range(1,2*k,2):dp[i] = -prices[0]for i in range(1,len(prices)):for j in range(1,2*k + 1):if j % 2:dp[j] = max(dp[j],dp[j-1]-prices[i])else:dp[j] = max(dp[j],dp[j-1]+prices[i])return dp[2*k]

1.5.5、309. 最佳买卖股票时机含冷冻期

309. 最佳买卖股票时机含冷冻期

class Solution:def maxProfit(self, prices: List[int]) -> int:n = len(prices)if n == 0:return 0dp = [[0] * 4 for _ in range(n)]dp[0][0] = -prices[0] #持股票for i in range(1, n):dp[i][0] = max(dp[i-1][0], max(dp[i-1][3], dp[i-1][1]) - prices[i])dp[i][1] = max(dp[i-1][1], dp[i-1][3])dp[i][2] = dp[i-1][0] + prices[i]dp[i][3] = dp[i-1][2]return max(dp[n-1][3], dp[n-1][1], dp[n-1][2])

1.5.6、714. 买卖股票的最佳时机含手续费

714. 买卖股票的最佳时机含手续费

class Solution:def maxProfit(self, prices: List[int], fee: int) -> int:n = len(prices)dp = [[0] * 2 for _ in range(n)]dp[0][0] = -prices[0] #持股票for i in range(1, n):dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i])dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i] - fee)return max(dp[-1][0], dp[-1][1])


1.5、子序列问题

1.5.1、子序列(不连续)

1.5.1.1、300. 最长上升子序列

力扣题目链接
从左开始遍历每个点,再从左到该点进行遍历和比较,比它小则累加,这种加法保证了是有序的。

class Solution:def lengthOfLIS(self, nums: List[int]) -> int:# 全部初始化为最小的子序列1,用来存储该位置的最小子序列dp = [1]*len(nums)dp[0]=1for i in range(1, len(nums)):# 因为是非连续的子序列,所以需要遍历前面所有点for j in range(i):if nums[j]<nums[i]:dp[i] = max(dp[i], dp[j]+1)return max(dp)

1.5.1.2、1143. 最长公共子序列

力扣题目链接

注意与 1.5.2.2、718. 最长重复子数组 的区分,可以先尝试做718. 最长重复子数组再来尝试这题。

这题要考虑不连续的公共子串,匹配不相等时,该如何处理值:
在这里插入图片描述
有三个方向推到dp[i][j],斜对角为匹配相等时,前面匹配的字符+这次匹配到一次也就是1;匹配不相等时,上和左取最大值,取上可能这一行都不匹配,取左可能前面有匹配的而后面不匹配。

class Solution:def longestCommonSubsequence(self, text1: str, text2: str) -> int:dp = [[0]*(len(text2)+1) for _ in range(len(text1)+1)]for i in range(1, len(text1)+1):for j in range(1, len(text2)+1):if text1[i-1] == text2[j-1]:# 匹配相等,把斜上角的值填过来+1,斜上角为各个字符串上一步匹配相等的次数dp[i][j] = dp[i-1][j-1] + 1else:# dp[i][j-1],有过相等但后续匹配不相等时,把左一个值向右填充# dp[i-1][j],匹配完全不相等,把上一个值向下填充# dp[i][j-1], text2在该字母上的最大子序列# dp[i-1][j], text1在该字母上的最大子序列# 两个字母不相等,dp[i][j] = max(dp[i-1][j], dp[i][j-1])return dp[-1][-1]

在这里插入图片描述
这一题是公共连续序列,对于其拓展,还有题型 最长公共子串


1.5.1.3、1035. 不相交的线

力扣题目链接
连线是有序的,实际上是求最大公共子序列。
求两个字符串的公共子序列,则需要二元数组。

class Solution:def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int:dp = [[0]*(len(nums2)+1) for _ in range(len(nums1)+1)]for i in range(1, len(nums1)+1):for j in range(1, len(nums2)+1):if nums1[i-1]==nums2[j-1]:dp[i][j] = dp[i-1][j-1]+1else:dp[i][j] = max(dp[i-1][j], dp[i][j-1])print(dp)return dp[-1][-1]

这一题的本质是与 1.5.1.2、1143. 最长公共子序列 相同,代码与过程分析相同。


1.5.2、子序列(连续)

1.5.2.1、674. 最长连续递增序列

力扣题目链接

这道题注意和上个单元的 1.5.1.1、300. 最长上升子序列 的区分,连续和不连续。

class Solution:def findLengthOfLCIS(self, nums: List[int]) -> int:dp = [1]*len(nums)# 这里只用一层循环,因为是连续子串,所以只用比较相邻的值就行for i in range(1, len(nums)):if nums[i]>nums[i-1]:dp[i] = dp[i-1]+1return max(dp)

或者 增加些内存来减少用时:

class Solution:def findLengthOfLCIS(self, nums: List[int]) -> int:dp = [1]*len(nums)result = 1for i in range(1, len(nums)):if nums[i]>nums[i-1]:dp[i] = dp[i-1]+1if dp[i]>result:result = dp[i]return result

在这里插入图片描述


1.5.2.2、718. 最长重复子数组

力扣题目链接

class Solution:def findLength(self, nums1: List[int], nums2: List[int]) -> int:# 这里创建二维dp table,记录两个字符串的匹配数(注意i与j的顺序)dp = [[0]*(len(nums2)+1) for _ in range(len(nums1)+1)]# 取最大值result = 0# 两层循环,遍历两个字符串,顺序可以调换for i in range(1, len(nums1)+1):for j in range(1, len(nums2)+1):# 判断当前索引的值是否相等if nums1[i-1]==nums2[j-1]:# 等于各个前面一个的值的匹配数+1dp[i][j] = dp[i-1][j-1]+1if dp[i][j]>result:result = dp[i][j]return result

在这里插入图片描述
时间复杂度:O(n×m)O(n × m)O(n×m),n 为A长度,m为B长度
空间复杂度:O(n×m)O(n × m)O(n×m)

滚动数组:


时间复杂度:O(n×m)O(n × m)O(n×m),n 为A长度,m为B长度
空间复杂度:O(m)O(m)O(m)


1.5.2.3、53. 最大子序和

力扣题目链接

class Solution:def maxSubArray(self, nums: List[int]) -> int:# 从0到第i个数的最大值dp = [0]*(len(nums))# 初始化,第一个dp[0] = nums[0]# 从第二个开始遍历for i in range(1, len(nums)):# 前一个最大值+该索引的值是否大,大就相加,不大就只保留索引值dp[i] = max(nums[i], nums[i]+dp[i-1])return max(dp)

在这里插入图片描述


1.5.3、编辑距离

1.5.3.1、392. 判断子序列

力扣题目链接

思路同上,两个字符串比较,最大公共子序列

class Solution:def isSubsequence(self, s: str, t: str) -> bool:dp = [[0]*(len(t)+1) for _ in range(len(s)+1)]for i in range(1, len(s)+1):for j in range(1, len(t)+1):if s[i-1]==t[j-1]:dp[i][j] = dp[i-1][j-1]+1else:# dp[i-1][j]表示不匹配时,是否把上一个结果往下移动,# 不加会又从0开始,加会少+1一次,总之,结果都会比length少dp[i][j] = dp[i][j-1]# 也可以:dp[i][j] = max(dp[i][j-1], dp[i-1][j])if dp[-1][-1]==len(s):return Truereturn False

1.5.3.2、115. 不同的子序列

力扣题目链接

class Solution:def numDistinct(self, s: str, t: str) -> int:# dp table 出现的个数dp = [[0]*(len(s)+1) for _ in range(len(t)+1)]# t若为空,则在s中都能出现一次,所以第一行初始化为1for i in range(len(s)+1):dp[0][i] = 1# s为空,则不能被t的任何任何字母组成# 所以 dp[i][0] = 0不变# i到前面的字母(t) 出现在 j到之前的字母(s) 出现的次数for i in range(1, len(t)+1):for j in range(1, len(s)+1):if t[i-1] == s[j-1]:# dp[i-1][j-1] 两个字符串s与t,退一个字母匹配的次数# dp[i][j-1] s字符串 向左退一次匹配的次数,前面有重复的就会累加dp[i][j] = dp[i-1][j-1]+dp[i][j-1]else:dp[i][j] = dp[i][j-1]return dp[-1][-1]

在这里插入图片描述


1.5.3.3、583. 两个字符串的删除操作

力扣题目链接
确定递推公式
当word1[i - 1] 与 word2[j - 1]相同的时候
当word1[i - 1] 与 word2[j - 1]不相同的时候
当word1[i - 1] 与 word2[j - 1]相同的时候,dp[i][j] = dp[i - 1][j - 1];

当word1[i - 1] 与 word2[j - 1]不相同的时候,有三种情况:

情况一:删word1[i - 1],最少操作次数为dp[i - 1][j] + 1

情况二:删word2[j - 1],最少操作次数为dp[i][j - 1] + 1

情况三:同时删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i - 1][j - 1] + 2

那最后当然是取最小值,所以当word1[i - 1] 与 word2[j - 1]不相同的时候,递推公式:dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1})

class Solution:def minDistance(self, word1: str, word2: str) -> int:# 建表dp = [[0]*(len(word2)+1) for _ in range(len(word1)+1)]# 初始化for i in range(len(word2)+1):dp[0][i]=ifor i in range(len(word1)+1):dp[i][0]=i# 填表for i in range(1, len(word1)+1):for j in range(1, len(word2)+1):if word1[i-1] == word2[j-1]:dp[i][j] = dp[i-1][j-1]else:# word1删除,word2删除,word1和word2都删除一个dp[i][j] = min(dp[i-1][j]+1, dp[i][j-1]+1, dp[i-1][j-1]+2)return dp[-1][-1]

在这里插入图片描述


1.5.3.4、72. 编辑距离

力扣题目链接

class Solution:def minDistance(self, word1: str, word2: str) -> int:dp = [[0]*(len(word2)+1) for _ in range(len(word1)+1)]# 初始化,删除为空值需要的步数for i in range(len(word2)+1):dp[0][i] = ifor i in range(len(word1)+1):dp[i][0] = i# 循环for i in range(1, len(word1)+1):for j in range(1, len(word2)+1):if word1[i-1] == word2[j-1]:dp[i][j] = dp[i-1][j-1]else:# 删除word1,删除word2,替换word1和word2. +1都是一次操作dp[i][j] = min(dp[i-1][j]+1, dp[i][j-1]+1, dp[i-1][j-1]+1)return dp[-1][-1]

在这里插入图片描述



1.5.4、回文

1.5.4.1、647. 回文子串

力扣题目链接

  1. 确定dp数组(dp table)以及下标的含义
    布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。
  2. 确定递推公式
    整体上是两种,就是s[i]与s[j]相等,s[i]与s[j]不相等这两种。
    当s[i]与s[j]不相等,dp[i][j]一定是false。
    当s[i]与s[j]相等时,有如下三种情况:
    情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
    情况二:下标i 与 j相差为1,例如aa,也是回文子串
    情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。

递归公式:

if s[i] == s[j]:if j-i<=1:result+=1dp[i][j] = Trueelse if dp[i+1][j-1]:result+=1dp[i][j] = True

result就是统计回文子串的数量。

这里没有列出当s[i]与s[j]不相等的时候,因为在下面dp[i][j]初始化的时候,就初始为false。

  1. dp数组如何初始化
    dp[i][j]不能初始化为true,这就表示全都匹配上了。所以dp[i][j]初始化为false。

  2. 确定遍历顺序
    首先从递推公式中可以看出,情况三是根据dp[i + 1][j - 1]是否为true,在对dp[i][j]进行赋值true的。

dp[i + 1][j - 1] 在 dp[i][j]的左下角,如图:
在这里插入图片描述
如果这矩阵是从上到下,从左到右遍历,那么会用到没有计算过的dp[i + 1][j - 1],也就是根据不确定是不是回文的区间[i+1,j-1],来判断了[i,j]是不是回文,那结果一定是不对的。

所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的。

有的代码实现是优先遍历列,然后遍历行,其实也是一个道理,都是为了保证dp[i + 1][j - 1]都是经过计算的。

  1. 举例推导dp数组
    举例,输入:“aaa”,dp[i][j]状态如下:
    在这里插入图片描述
    图中有6个true,所以就是有6个回文子串。

注意因为dp[i][j]的定义,所以j一定是大于等于i的,那么在填充dp[i][j]的时候一定是只填充右上半部分。

综上,代码如下:

动态规划:

class Solution:def countSubstrings(self, s: str) -> int:# 暴力求解是双循环,所以这里创建仿造,创建二元数组dp = [[False]*(len(s)) for _ in range(len(s))]result = 0# 从后往前遍历for i in range(len(s)-1, -1, -1):for j in range(i, len(s)):# 字符串字母相同if s[i]==s[j]:# 自身或者相邻if j-i<=1:dp[i][j]=Trueresult+=1# 不相邻,相隔大于1# 该字符串是否为回文=首字母+1,结尾字母-1# 就是前后都往内部缩一个字母是否为回文elif dp[i+1][j-1]:dp[i][j]=Trueresult+=1return result

时间复杂度:O(n^2)
空间复杂度:O(n^2)

暴力遍历:
双层循环,每次后移一个字母判断回文

class Solution:def countSubstrings(self, s: str) -> int:result = 0# 暴力遍历for i in range(1, len(s)+1):for j in range(i):if s[j:i] == s[j:i][::-1]:result+=1return result

双指针法:

class Solution:def countSubstrings(self, s: str) -> int:result = 0for i in range(len(s)):# 以i为中心result += self.extend(s, i, i, len(s))# 以i+1为中心result += self.extend(s, i, i+1, len(s))return resultdef extend(self, s, i, j, n):res = 0while i>=0 and j<n and s[i]==s[j]:i-=1j+=1res+=1return res

时间复杂度:O(n^2)
空间复杂度:O(1)


1.5.4.2、最长回文子串

HJ32 密码截取 - 牛客网

import sysfor line in sys.stdin:line = line[:-1]# dp[i][j]表示从i到j是否为回文# create dp tabledp = [[0]*len(line) for _ in range(len(line))]# initial tablefor i in range(len(line)):dp[i][i]=1max_long = 0# go through table# 一头一尾开始遍历,行逆序for i in range(len(line)-1, -1, -1):for j in range(i+1, len(line)):# 字母相等if line[i] == line[j]:# 判断中间字符是否相等,或者两个字符相邻,无中间字符if dp[i+1][j-1] or j-i==1:# 加上i和j上的两个字符dp[i][j] = dp[i+1][j-1]+2# 去掉非连续# else:# dp[i][j] = max(dp[i+1][j], dp[i][j-1])# 每一次起点遍历,保存最大值if max_long<max(dp[i]):max_long = max(dp[i])print(max_long)

这里,可以像上题一样的写法,先不初始化,双循环内的判断条件可以改为,1.奇:i-j=0,2.偶:i-j=1,3.大于2的情况,除开两端的中间为回文,即:
在这里插入图片描述


1.5.4.3、516. 最长回文子序列

力扣题目链接
这一题与上一题的区别:回文子串是要连续的,回文子序列可不是连续的。

  1. 确定dp数组(dp table)以及下标的含义
    dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]。
  2. 确定递推公式
    如果s[i]与s[j]相同,那么dp[i][j] = dp[i + 1][j - 1] + 2
    在这里插入图片描述
    如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子串的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。
    加入s[j]的回文子序列长度为dp[i + 1][j]。
    加入s[i]的回文子序列长度为dp[i][j - 1]。
    那么dp[i][j]一定是取最大的,即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
    在这里插入图片描述
  3. dp数组如何初始化
    首先要考虑当i 和j 相同的情况,从递推公式:dp[i][j] = dp[i + 1][j - 1] + 2; 可以看出 递推公式是计算不到 i 和j相同时候的情况。
    所以需要手动初始化一下,当i与j相同,那么dp[i][j]一定是等于1的,即:一个字符的回文子序列长度就是1。
    其他情况dp[i][j]初始为0就行,这样递推公式:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); 中dp[i][j]才不会被初始值覆盖。
for i in range(len(s)):dp[i][i]=1
  1. 确定遍历顺序
    从递推公式dp[i][j] = dp[i + 1][j - 1] + 2 和 dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]) 可以看出,dp[i][j]是依赖于dp[i + 1][j - 1] 和 dp[i + 1][j],
    也就是从矩阵的角度来说,dp[i][j] 下一行的数据。 所以遍历i的时候一定要从下到上遍历,这样才能保证,下一行的数据是经过计算的。
    递推公式:dp[i][j] = dp[i + 1][j - 1] + 2,dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]) 分别对应着下图中的红色箭头方向,如图:
    在这里插入图片描述
for i in range(len(s)-1, -1, -1):for j in range(i+1, len(s)):if s[i] == s[j]:dp[i][j] = dp[i+1][j-1] + 2else:dp[i][j] = max(dp[i+1][j], dp[i][j-1])
  1. 举例推导dp数组
    输入s:“cbbd” 为例,dp数组状态如图:
    在这里插入图片描述
class Solution:def longestPalindromeSubseq(self, s: str) -> int:# dp[i][j]表示从i到j的最长子序列dp = [[0]*len(s) for _ in range(len(s))]# 初始化,对角线为1for i in range(len(s)):dp[i][i]=1# 循环填表,从下到上,从左到右for i in range(len(s)-1, -1, -1):for j in range(i+1, len(s)):if s[i]==s[j]:dp[i][j] = dp[i+1][j-1] + 2else:dp[i][j] = max(dp[i+1][j], dp[i][j-1])return dp[0][-1]


相关文章:

算法刷题总结 (四) 动态规划

算法总结4 动态规划一、动态规划1.1、基础问题11.1.1、509. 斐波那契数列1.1.2、70. 爬楼梯1.1.3、746. 使用最小花费爬楼梯1.2、基础问题21.2.1、62. 不同路径1.2.2、63. 不同路径Ⅱ1.2.3、343. 整数拆分1.2.4、96. 不同的二叉搜索树1.3、背包问题1.3.1、01背包1.3.1.1、单次选…...

Grafana 转换数据的工具介绍

转换数据 Grafana 可以在数据显示到面板前对数据进行处理 1、点击Transform选项卡 2、选择要使用的转换类型&#xff0c;不同的转换类型配置不同 3、要新增转换类型&#xff0c;点击Add transformation 4、使用右上角调式按钮可以调式转换 支持的转换类型&#xff1a; Add f…...

Linux 学习笔记

一、 概述 1. 操作系统 ① 计算机由硬件和软件组成 ② 操作系统属于软件范畴&#xff0c;主要作用是协助用户调度硬件工作&#xff0c;充当用户和计算机硬件之间的桥梁 ③ 常见的操作系统 &#x1f920; PC端&#xff1a;Windows、Linux、MacOS&#x1f920; 移动端&#…...

HTML注入专精整理

目录 HTML注入介绍 抽象解释 HTML注入的影响 HTML注入与XSS的区别 HTML元素流程图...

看完这篇我不信你不会二叉树的层序遍历【C语言】

目录 实现思路 代码实现 之前介绍了二叉树的前、中、后序三种遍历&#xff0c;采用的是递归的方式。今天我们来学习另外一种遍历方式——层序遍历。层序遍历不容小觑&#xff0c;虽然实现方法并不难&#xff0c;但是它所采取的思路是很值得学习的&#xff0c;与前三者不同&am…...

案例17-环境混用带来的影响

目录一、背景介绍背景事故二、思路&方案三、过程四、总结nginx做转发fastdfs&#xff08;文件上传下载&#xff09;五、升华一、背景介绍 本篇博客主要介绍开发中项目使用依赖项环境闭一只带来的恶劣影响&#xff0c;在错误中成长进步。 背景 本公司另外一个产品开发God…...

知识蒸馏论文阅读:DKD算法笔记

标题&#xff1a;Decoupled Knowledge Distillation 会议&#xff1a;CVPR2022 论文地址&#xff1a;https://ieeexplore.ieee.org/document/9879819/ 官方代码&#xff1a;https://github.com/megvii-research/mdistiller 作者单位&#xff1a;旷视科技、早稻田大学、清华大学…...

Sentinel架构篇 - 熔断降级

熔断降级 概念 除了流量控制以外&#xff0c;对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用其它模块&#xff0c;可能是一个远程服务、数据库、或者第三方 API 等。然而&#xff0c;被依赖的服务的稳定性是不能保证的。如果依赖的服…...

shell脚本的一些记录 与jenkins的介绍

shell 脚本的执行 sh ***.sh shell脚本里面的命令 其实就是终端执行一些命令 shell 连接服务器 可以直接ssh连接 但是这样最好是无密码的 不然后面的命令就不好写了 换而言之有密码得 不好写脚本 需要下载一些expect的插件之类的才可以 判断语句 的示例 需要注意的是…...

JVM的了解与学习

一:jvm是什么 jvm是java虚拟机java Virtual Machine的缩写 jdk包含jre和java DevelopmentTools 二:什么是java虚拟机 虚拟机是一种抽象化的计算机,通过在实际的计算机上仿真模拟各种计算机功能来实现的。java虚拟机有自己完善的硬体结构,如处理器、堆栈、寄存器等,还有…...

提升数字品牌的5个技巧

“品牌”或“品牌推广”的概念通常用于营销。因为建立您的企业品牌对于产品来说极其重要&#xff0c;品牌代表了您与客户互动的身份和声音。今天&#xff0c;让我们来看看在数字领域提升品牌的一些有用的技巧。如何在数字领域提升您的品牌&#xff1f;在了解这些技巧之前&#…...

java通过反射获取加了某个注解的所有的类

有时候我们会碰到这样的情况&#xff1a;有n个场景&#xff0c;每个场景都有自己的逻辑&#xff0c;即n个处理逻辑&#xff0c;这时候我们就需要通过某个参数的值代表这n个场景&#xff0c;然后去加载每个场景不同的bean对象&#xff0c;即不同的类&#xff0c;这些类中都有一个…...

Warshall算法

&#x1f680;write in front&#x1f680; &#x1f4dc;所属专栏&#xff1a;> 算法 &#x1f6f0;️博客主页&#xff1a;睿睿的博客主页 &#x1f6f0;️代码仓库&#xff1a;&#x1f389;VS2022_C语言仓库 &#x1f3a1;您的点赞、关注、收藏、评论&#xff0c;是对我…...

vector中迭代器失效的问题及解决办法

目录 vector常用接口 vector 迭代器失效问题 vector中深浅拷贝问题 vector的数据安排以及操作方式&#xff0c;与array非常相似。两者的唯一差别在于空间的运用的灵活性。array 是静态空间&#xff0c;一旦配置了就不能改变&#xff1b;要换个大(或小) 一点的房子&#x…...

【蓝桥杯刷题训练营】day05

1 数的分解 拆分成3个数相加得到该数 然后采用了一种巨愚蠢的办法&#xff1a; int main() {int count 0;int a 2;int b 0;int c 1;int d 9;int a1, a2, a3;int c1, c2, c3;int d1, d2, d3;for (a1 0; a1 < 2; a1){for (a2 0; a2 < 2; a2){for (a3 0; a3 <…...

线程中断interrupt导致sleep产生的InterruptedException异常

强制当前正在执行的线程休眠&#xff08;暂停执行&#xff09;&#xff0c;以“减慢线程”。 Thread.sleep(long millis)和Thread.sleep(long millis, int nanos)静态方法当线程睡眠时&#xff0c;它睡在某个地方&#xff0c;在苏醒之前不会返回到可运行状态。 当睡眠时间到期…...

ubuntu的快速安装与配置

文章目录前言一、快速安装二 、基础配置1 Sudo免密码2 ubuntu20.04 pip更新源3 安装和配置oneapi(infort/mpi/mkl) apt下载第一次下载的要建立apt源apt下载&#xff08;infort/mpi/mkl)4 安装一些依赖库等5 卸载WSLpython总结前言 win11系统 ubuntu20.04 提示&#xff1a;以下…...

人工智能AI工具汇总(AIGC ChatGPT时代个体崛起)

NameCategoryWebsiteDescription描述《AIGC时代&#xff1a;超级个体的崛起》小报童https://xiaobot.net/p/SuperIndividual 介绍AIGC&#xff0c;ChatGPT&#xff0c;使用技巧与搞钱方式。Masterpiece Studio3Dhttps://masterpiecestudio.comSimplifying 3D Creation with AI…...

【rust-grpc-proxy】在k8s中,自动注入代理到pod中,再不必为grpc调试而烦恼

目录前言原理sidecarwebhook实现安装k8s设置webhook使用尾语前言 rust-grpc-proxy 目前功能基本完善。是时候上环境开始应用了。 之前考虑是gateway模式或者sidecar模式。 思考良久之后&#xff0c;觉得两种模式都有使用场景&#xff0c;那就都支持。本次就带来sidecar模式的食…...

VisualStudio2022制作多项目模板及Vsix插件

一、安装工作负载 在vs2022上安装“visual studio扩展开发 ”工作负载 二、制作多项目模板 导出项目模板这个我就不再多说了&#xff08;项目→导出模板→选择项目模板&#xff0c;选择要导出的项目→填写模板信息→完成&#xff09;。 1.准备模板文件 将解决方案中的多个…...

仿写简单IOC

目录 TestController类: UserService类: 核心代码SpringIOC&#xff1a; Autowired和Component注解 SpringIOCTest 类 ​编辑 总结&#xff1a; TestController类: Component public class TestController {Autowiredprivate UserService userService;public void test…...

liunx下安装node exporter

1 建立文件夹 cd /opt mkdir software 下载最新的包&#xff0c;并解压 https://prometheus.io/download/ 下载 curl -LO https://github.com/prometheus/node_exporter/releases/download/v0.18.1/node_exporter-0.18.1.linux-amd64.tar.gz 3.解压 tar -xvf node_exporter-0.…...

lambda函数

Lambda(函数指针)lambda 是c11非常重要也是最常用的特性之一&#xff0c;他有以下优点&#xff1a;可以就地匿名定义目标函数或函数对象&#xff0c;不需要额外写一个函数lambda表达式是一个匿名的内联函数lambda表达式定义了一个匿名函数&#xff0c;语法如下&#xff1a;[cap…...

【Python入门第二十七天】Python 日期

Python 日期 Python 中的日期不是其自身的数据类型&#xff0c;但是我们可以导入名为 datetime 的模块&#xff0c;把日期视作日期对象进行处理。 实例 导入 datetime 模块并显示当前日期&#xff1a; import datetimex datetime.datetime.now() print(x)运行实例 2023-0…...

C++基础知识【5】数组和指针

目录 一、概述 数组 指针 二、数组 2.1、数组的声明 2.2、数组的初始化 2.3、数组的访问 2.4、多维数组 2.5、数组作为函数参数 三、指针 3.1、指针的声明 3.2、指针的赋值 3.3、指针的访问 3.4、指针运算 3.5、指针数组和数组指针 3.6、二级指针 四、数组和指…...

Vim使用操作命令笔记

Vim使用操作命令笔记在普通模式下&#xff0c;输入 : help tutor 就可以进入vim的教学       在 terminal 中输入 vim 文件名 就可以打开文件    vim有两种模式   normal mode &#xff08;普通模式&#xff09;→ 指令操作   insert mode &#xff08;输入模式&…...

【论文阅读】Robust Multi-Instance Learning with Stable Instances

1、摘要与引言 以往的MIL算法遵循i.i.d假设&#xff1a;训练样本与测试样本都分别来自于同一分布中&#xff0c;而这一假设往往与现实应用中有所出入。研究人员通过计算训练样本与测试样本之间的密度比对训练样本进行加权&#xff0c;以解决分布变化带来的问题。 分布的变化发…...

洛谷 P5116 [USACO18DEC]Mixing Milk B

题目链接&#xff1a;P5116 [USACO18DEC]Mixing Milk B - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 题目描述 农业&#xff0c;尤其是生产牛奶&#xff0c;是一个竞争激烈的行业。Farmer John 发现如果他不在牛奶生产工艺上有所创新&#xff0c;他的乳制品生意可能就会受…...

华为OD机试 - 最左侧冗余覆盖子串(C 语言解题)【独家】

最近更新的博客 华为od 2023 | 什么是华为od,od 薪资待遇,od机试题清单华为OD机试真题大全,用 Python 解华为机试题 | 机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南)华为od机试,独家整理 已参加机试人员的实战技巧文章目录 使用说明本期题目:最左侧冗…...

《Netty》从零开始学netty源码(三)之SelectorProvider

...