代码随想录python笔记9 动态规划

理论基础

动态规划

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

和贪心的区别

所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的,

例如:有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

动态规划中dp[j]是由dp[j-weight[i]]推导出来的,然后取max(dp[j], dp[j - weight[i]] + value[i])。

但如果是贪心呢,每次拿物品选一个最大的或者最小的就完事了,和上一个状态没有关系

动态规划五步曲

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

动态规划应该如何debug

找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的!

写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍

先思考这三个问题:

  • 这道题目我举例推导状态转移公式了么?
  • 我打印dp数组的日志了么?
  • 打印出来了dp数组和我想的一样么?

509. 斐波那契数

力扣题目链接(opens new window)

斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: F(0) = 0,F(1) = 1 F(n) = F(n - 1) + F(n - 2),其中 n > 1 给你n ,请计算 F(n) 。

示例 1:

  • 输入:2
  • 输出:1
  • 解释:F(2) = F(1) + F(0) = 1 + 0 = 1

递推数组

class Solution:
    def fib(self, n: int) -> int:
        if n < 2 : return n
        # 创建 dp table 
        dp = [0] * (n + 1)
        # 初始化 dp 数组
        dp[0], dp[1] = 0, 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 < 2 : return n
        # 只需要维护两个数值就可以了,不需要记录整个序列
        a, b, c = 0, 1, 0 # 初始化
        for i in range(1, n):
            a, b = b, a + b # dp[i] = dp[i - 1] + dp[i - 2]
        return b

递归实现

class Solution:
    def fib(self, n: int) -> int:
        if n < 2 : return n
        # 递归
        return self.fib(n - 1) + self.fib(n - 2)

70. 爬楼梯

力扣题目链接(opens new window)

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1:

  • 输入: 2
  • 输出: 2
  • 解释: 有两种方法可以爬到楼顶。
    • 1 阶 + 1 阶
    • 2 阶

递推数组

class Solution:
    def climbStairs(self, n: int) -> int:
        # 记录整个数组,空间复杂度为O(n)
        dp = [0] * (n + 2) 
        dp[1], dp[2] = 1, 2
        for i in range(3, n + 1):
            dp[i] = dp[i - 1] + dp[i - 2]
        return dp[n]

状态转移

class Solution:
    def climbStairs(self, n: int) -> int:
        # 只记录两个状态,空间复杂度为O(1)
        if n < 3: return n
        a, b = 1, 2
        for i in range(3, n+1):
            a, b = b, a + b
        return b

746. 使用最小花费爬楼梯

力扣题目链接(opens new window)

旧题目描述

数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。

每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。

请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。

示例 1:

  • 输入:cost = [10, 15, 20]
  • 输出:15
  • 解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15 。

示例 2:

  • 输入:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
  • 输出:6
  • 解释:最低花费方式是从 cost[0] 开始,逐个经过那些 1 ,跳过 cost[3] ,一共花费 6 。

提示:

  • cost 的长度范围是 [2, 1000]。
  • cost[i] 将会是一个整型数据,范围为 [0, 999] 。

数组递归

class Solution:
    def minCostClimbingStairs(self, cost: List[int]) -> int:
        # 使用数组记录
        dp = [0] * len(cost)
        dp[0], dp[1] = cost[0], cost[1]
        for i in range(2, len(cost)):
            dp[i] = min(dp[i-1], dp[i-2]) + cost[i]
        return min(dp[len(cost)-1], dp[len(cost)-2])

状态转移

class Solution:
    def minCostClimbingStairs(self, cost: List[int]) -> int:
        # 使用前后状态记录
        a, b = cost[0], cost[1]
        for i in range(2, len(cost)):
            a, b = b, min(a, b) + cost[i]
        return min(a, b)

62.不同路径

力扣题目链接(opens new window)

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例 1:

  • 输入:m = 3, n = 7
  • 输出:28

示例 2:

  • 输入:m = 2, n = 3
  • 输出:3

解释: 从左上角开始,总共有 3 条路径可以到达右下角。

  1. 向右 -> 向右 -> 向下
  2. 向右 -> 向下 -> 向右
  3. 向下 -> 向右 -> 向右

提示:

  • 1 <= m, n <= 100
  • 题目数据保证答案小于等于 2 * 10^9

二维数组

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        # 二维数组动态规划
        dp = [[1 for i in range(n)] for j 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]

一维滚动数组

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        # 一维(滚动)宿主
        cur = [1] * n
        for i in range(1, m):
            for j in range(1, n):
                cur[j] += cur[j-1]
        return cur[-1]

数学方法

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        # 数学方法
        return int(math.factorial(m+n-2)/math.factorial(m-1)/math.factorial(n-1))

63. 不同路径 II

力扣题目链接(opens new window)

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

代码随想录python笔记9 动态规划_第1张图片

网格中的障碍物和空位置分别用 1 和 0 来表示。

示例 1:

img
  • 输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
  • 输出:2 解释:
  • 3x3 网格的正中间有一个障碍物。
  • 从左上角到右下角一共有 2 条不同的路径:
    1. 向右 -> 向右 -> 向下 -> 向下
    2. 向下 -> 向下 -> 向右 -> 向右

提示:

  • m == obstacleGrid.length
  • n == obstacleGrid[i].length
  • 1 <= m, n <= 100
  • obstacleGrid[i][j] 为 0 或 1
class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        m, n = len(obstacleGrid), len(obstacleGrid[0])
        dp = [[0 for i in range(n)]for j in range(m)]
        if obstacleGrid[0][0] == 1:
            return 0
        else: dp[0][0] = 1
        for i in range(1, n):
            if obstacleGrid[0][i] == 0:
                dp[0][i] = 1
            else:break
        for i in range(1, m):
            if obstacleGrid[i][0] == 0:
                dp[i][0] = 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][j - 1] + dp[i - 1][j]
        return dp[-1][-1]
class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        # 优化空间
        m, n = len(obstacleGrid), len(obstacleGrid[0])
        # 定义数组
        dp = [0]*n
        if obstacleGrid[0][0] == 1:
            return 0
        else: dp[0] = 1
        # 递推公式
        for i in range(m):
            for j in range(n):
                if obstacleGrid[i][j] == 1: 
                    dp[j]=0
                elif obstacleGrid[i][j] == 0 and j > 0:
                    dp[j] = dp[j] + dp[j - 1]
        return dp[n-1]

343. 整数拆分

力扣题目链接(opens new window)

给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。

示例 1:

  • 输入: 2
  • 输出: 1
  • 解释: 2 = 1 + 1, 1 × 1 = 1。

示例 2:

  • 输入: 10
  • 输出: 36
  • 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
  • 说明: 你可以假设 n 不小于 2 且不大于 58。
class Solution:
    def integerBreak(self, n: int) -> int:
        dp = [0] * (n + 1)
        dp[2] = 1
        for i in range(3, n + 1):
            # 假设对正整数 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]
            for j in range(1, i / 2 + 1):
                dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]))
        return dp[n]

96.不同的二叉搜索树

力扣题目链接(opens new window)

给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?

示例:

img

dp[3],就是 元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量

遍历节点,按照左右子树不断递推(相同节点数量的二叉搜索树数量相同)

class Solution:
    def numTrees(self, n: int) -> int:
        dp = [0] * (n+1)
        dp[0] = dp[1] = 1
        for i in range(2, n+1):
            for j in range(1,i+1):
                dp[i] += dp[i-j] * dp[j-1]
        return dp[n]

01背包理论基础

416.分割等和子集1

背包问题,大家都知道,有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

背包问题有多种背包方式,常见的有:01背包、完全背包、多重背包、分组背包和混合背包等等。

要注意题目描述中商品是不是可以重复放入。

即一个商品如果可以重复多次放入是完全背包,而只能放入一次是01背包

二维dp数组01背包

背包最大重量为4。

物品为:

重量 价值
物品0 1 15
物品1 3 20
物品2 4 30

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

以下讲解和图示中出现的数字都是以这个例子为例。

依然动规五部曲分析一波。

  1. 确定dp数组以及下标的含义

对于背包问题,有一种写法, 是使用二维数组,即dp[ i ] [ j ] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。

动态规划-背包问题1

切记dp数组的含义!

  • 不放物品i:由dp[i - 1] [j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1] [j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以被背包内的价值依然和前面相同。)
  • 放物品i:由dp[i - 1] [j - weight[i]]推出,dp[i - 1] [j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1] [j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值

所以递归公式: dp[i] [j] = max(dp[i - 1] [ j ], dp[i - 1] [j - weight[i]] + value[i]);

一维滚动数组

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

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

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

dp[i] [j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少

所以递归公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

416. 分割等和子集

力扣题目链接(opens new window)

题目难易:中等

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

注意: 每个数组中的元素不会超过 100 数组的大小不会超过 200

示例 1:

  • 输入: [1, 5, 11, 5]
  • 输出: true
  • 解释: 数组可以分割成 [1, 5, 5] 和 [11].

示例 2:

  • 输入: [1, 2, 3, 5]
  • 输出: false
  • 解释: 数组不能分割成两个元素和相等的子集.

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100

思路

只有确定了如下四点,才能把01背包问题套到本题上来。

  • 背包的体积为sum / 2
  • 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
  • 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
  • 背包中每一个元素是不可重复放入

dp[j]表示 背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dp[j]

代码随想录python笔记9 动态规划_第2张图片

当 dp[target] == target 的时候,背包就装满了

递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);

如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        if sum(nums) % 2 == 1:
            return False
        dp = [0] * 10001
        tar = int(sum(nums)/2)
        for i in range(len(nums)):
            for j in range(tar, nums[i]-1, -1):# 每一个元素一定是不可重复放入,所以从大到小遍历
                dp[j] = max(dp[j],dp[j-nums[i]]+nums[i])
        return dp[tar] == tar
  • 时间复杂度:O(m × n) , m是石头总重量(准确的说是总重量的一半),n为石头块数
  • 空间复杂度:O(m)

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

力扣题目链接(opens new window)

题目难度:中等

有一堆石头,每块石头的重量都是正整数。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

如果 x == y,那么两块石头都会被完全粉碎;

如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。

最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。

示例:

  • 输入:[2,7,4,1,8,1]
  • 输出:1

解释:

  • 组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
  • 组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
  • 组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
  • 组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

提示:

  • 1 <= stones.length <= 30
  • 1 <= stones[i] <= 1000

转化为分割子集问题。思路与上一题类似

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

背包总结

要求候选人先实现一个纯二维的01背包,如果写出来了,然后再问为什么两个for循环的嵌套顺序这么写?反过来写行不行?再讲一讲初始化的逻辑。

然后要求实现一个一维数组的01背包,最后再问,一维数组的01背包,两个for循环的顺序反过来写行不行?为什么?

二维:dp[i] [j] = max(dp[i - 1] [j], dp[i - 1] [j - weight[i]] + value[i]);

一维:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

二维01背包

  1. 确定dp数组以及下标的含义:dp[i] [j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。

  2. 确定递推公式:dp[i] [j] = max(dp[i - 1] [j], dp[i - 1] [j - weight[i]] + value[i]);

  3. dp数组如何初始化

// 初始化 dp
vector<vector<int>> dp(weight.size() + 1, vector<int>(bagWeight + 1, 0));
for (int j = bagWeight; j >= weight[0]; j--) {
    dp[0][j] = dp[0][j - weight[0]] + value[0];
}
  1. 确定遍历顺序:01背包二维dp数组在遍历顺序上,外层遍历物品 ,内层遍历背包容量 和 外层遍历背包容量 ,内层遍历物品 都是可以的!但是先遍历物品更好理解。代码如下:
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
    for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
        if (j < weight[i]) dp[i][j] = dp[i - 1][j]; // 这个是为了展现dp数组里元素的变化
        else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
    }
}
  1. 举例推导dp数组:背包最大重量为4,物品为:
重量 价值
物品0 1 15
物品1 3 20
物品2 4 30

动态规划-背包问题4

最终结果就是dp[2] [4]。

一维01背包

分析一下和二维dp数组有什么区别,在初始化和遍历顺序上又有什么差异?

01背包一维数组分析如下:

  1. 确定dp数组的定义:在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。

  2. 一维dp数组的递推公式

dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
  1. 一维dp数组如何初始化:如果物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。

  2. 一维dp数组遍历顺序

for(int i = 0; i < weight.size(); i++) { // 遍历物品
    for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
        dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
    }
}
  1. 举例推导dp数组:一维dp,分别用物品0,物品1,物品2 来遍历背包,最终得到结果如下:

动态规划-背包问题9

494.目标和

力扣题目链接(opens new window)

难度:中等

给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

示例:

  • 输入:nums: [1, 1, 1, 1, 1], S: 3
  • 输出:5

解释:

  • -1+1+1+1+1 = 3
  • +1-1+1+1+1 = 3
  • +1+1-1+1+1 = 3
  • +1+1+1-1+1 = 3
  • +1+1+1+1-1 = 3

一共有5种方法让最终目标和为3。

提示:

  • 数组非空,且长度不会超过 20 。
  • 初始的数组的和不会超过 1000 。
  • 保证返回的最终结果能被 32 位整数存下。

回溯法

会超时,类似组合总和,略

动态规划

假设加法的总和为x,那么减法对应的总和就是sum - x。

所以我们要求的是 x - (sum - x) = target

x = (target + sum) / 2

问题就转化:装满容量为x的背包,有几种方法

之前都是求容量为j的背包,最多能装多少。本题则是装满有几种方法。其实这就是一个组合问题了

  1. dp下标含义:dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法

  2. 递推公式:只要搞到nums[i]),凑成dp[j]就有dp[j - nums[i]] 种方法,dp[j] = 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的背包
    • 已经有一个 m (nums[i])的话,有 dp[0]中方法 凑成 容量为5的背包
  3. 初始化dp:dp[0] = 1

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

  5. 举例推导:输入:nums: [1, 1, 1, 1, 1], S= 3。bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4

    dp数组状态变化如下:

    494.目标和[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1uBFgruh-1679973227532)(C:\Users\wyw\AppData\Roaming\Typora\typora-user-images\image-20230302214936889.png)]

class Solution:
    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        if (target + sum(nums)) % 2 == 1 or sum(nums) < abs(target):
            return 0
        x = int((target + sum(nums)) / 2)
        dp = [0] * (x+1)
        dp[0] = 1
        for i in range(len(nums)):
            for j in range(x, nums[i] - 1, -1):
                dp[j] = dp[j] + dp[j - nums[i]]   # 填满容积为j的背包有dp[j]种方法
        return dp[x]
  • 时间复杂度:O(n × m),n为正数个数,m为背包容量

  • 空间复杂度:O(m),m为背包容量

组合问题求个数可以用动态规划,但是列出所有组合只能用回溯

474.一和零

力扣题目链接(opens new window)

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。

请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

示例 1:

  • 输入:strs = [“10”, “0001”, “111001”, “1”, “0”], m = 5, n = 3
  • 输出:4
  • 解释:最多有 5 个 0 和 3 个 1 的最大子集是 {“10”,“0001”,“1”,“0”} ,因此答案是 4 。 其他满足题意但较小的子集包括 {“0001”,“1”} 和 {“10”,“1”,“0”} 。{“111001”} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。

示例 2:

  • 输入:strs = [“10”, “0”, “1”], m = 1, n = 1
  • 输出:2
  • 解释:最大的子集是 {“0”, “1”} ,所以答案是 2 。

提示:

  • 1 <= strs.length <= 600
  • 1 <= strs[i].length <= 100
  • strs[i] 仅由 ‘0’ 和 ‘1’ 组成
  • 1 <= m, n <= 100

分析

本题中strs 数组里的元素就是物品,每个物品都是一个!,而m 和 n相当于是一个背包,两个维度的背包

本题是求 给定背包容量,装满背包最多有多少个物品

动规五部曲:

  1. 确定dp数组以及下标的含义:最多有i个0和j个1的strs的最大子集的个数为dp[i] [j]

  2. 确定递推公式:dp[i] [j] = max(dp[i] [j], dp[i - zero] [j - one] + 1)

    dp[i] [j] 可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1。

    dp[i] [j] 就可以是 dp[i - zero] [j - one] + 1

  3. dp数组如何初始化:dp = [[0]*(n+1) for _ in range(m+1)]

  4. 确定遍历顺序:先物品str,后容量(倒序遍历)

  5. 举例推导:以输入:[“10”,“0001”,“111001”,“1”,“0”],m = 3,n = 3为例,最后dp数组的状态如下所示:

    474.一和零
class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        dp = [[0]*(n+1) for _ in range(m+1)]
        for str in strs:
            one = str.count('1')
            zero = str.count('0')
            for i in range(m, zero - 1 , -1):
                for j in range(n, one - 1 , -1):
                    dp[i][j] = max(dp[i][j], dp[i - zero][j - one] + 1)
        return dp[m][n]

总结:

  • 纯 0 - 1 背包 (opens new window)是求 给定背包容量 装满背包 的最大价值是多少。
  • 416. 分割等和子集 (opens new window)是求 给定背包容量,能不能装满这个背包。
    • dp[j] 表示:背包总容量是j,放进物品后最大重量为dp[j]
    • 递推公式:dp[j] = max(dp[j], dp[j-nums[i]]+nums[i])
  • 1049. 最后一块石头的重量 II (opens new window)是求给定背包容量,最多能装多少
    • 同上, dp[j] = max(dp[j],dp[j-nums[i]]+nums[i])
  • 494. 目标和 (opens new window)是求 给定背包容量,装满背包有多少种方法
    • dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法
    • 递推公式:只要有nums[i]),凑成dp[j]就有dp[j - nums[i]] 种方法,dp[j] = dp[j] + dp[j - nums[i]]
  • 本题是求 给定背包容量,装满背包最多有多少个物品
    • dp[i] [j] 表示:容量为i、j 的最大物品个数为dp[i] [j]
    • 递推公式:dp = max(dp[i] [j], dp[i - zero] [j - one] + 1)

完全背包理论基础

可以重复出现,两次for都是从前往后遍历

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

518.零钱兑换II

力扣题目链接(opens new window)

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

示例 1:

  • 输入: amount = 5, coins = [1, 2, 5]
  • 输出: 4

解释: 有四种方式可以凑成总金额:

  • 5=5
  • 5=2+2+1
  • 5=2+1+1+1
  • 5=1+1+1+1+1

注意,你可以假设:

  • 0 <= amount (总金额) <= 5000
  • 1 <= coin (硬币面额) <= 5000
  • 硬币种类不超过 500 种
  • 结果符合 32 位符号整数

完全背包从前往后遍历

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

377. 组合总和 Ⅳ

力扣题目链接(opens new window)

难度:中等

给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。

示例:

  • nums = [1, 2, 3]
  • target = 4

所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1)

请注意,顺序不同的序列被视作不同的组合。

因此输出为 7。

完全背包组合问题

target(背包)放在外循环,将nums(物品)放在内循环,内循环从前到后遍历

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

70. 爬楼梯-进阶

力扣题目链接(opens new window)

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1: 输入: 2 输出: 2 解释: 有两种方法可以爬到楼顶。

  1. 1 阶 + 1 阶
  2. 2 阶

示例 2: 输入: 3 输出: 3 解释: 有三种方法可以爬到楼顶。

  1. 1 阶 + 1 阶 + 1 阶
  2. 1 阶 + 2 阶
  3. 2 阶 + 1 阶

一般动规解法

class Solution:
    def climbStairs(self, n: int) -> int:
        # 记录整个数组,空间复杂度为O(n)
        dp = [0] * (n + 2) 
        dp[1], dp[2] = 1, 2
        for i in range(3, n + 1):
            dp[i] = dp[i - 1] + dp[i - 2]
        return dp[n]
class Solution:
    def climbStairs(self, n: int) -> int:
        # 只记录两个状态,空间复杂度为O(1)
        if n < 3: return n
        a, b = 1, 2
        for i in range(3, n+1):
            a, b = b, a + b
        return b

完全背包进阶解法

class Solution:
    def climbStairs(self, n: int) -> int:
        # 完全背包
        dp = [0] * 46
        dp[0] = 1
        for j in range(n + 1):
            for i in range(1,3):
                dp[j] += dp[j-i]
        return dp[n]

322. 零钱兑换

力扣题目链接(opens new window)

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的。

示例 1:

  • 输入:coins = [1, 2, 5], amount = 11
  • 输出:3
  • 解释:11 = 5 + 5 + 1

示例 2:

  • 输入:coins = [2], amount = 3
  • 输出:-1

提示:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 2^31 - 1
  • 0 <= amount <= 10^4
class Solution:
    def coinChange(self, coins: List[int], amount: int) -> int:
        dp = [float('inf')] * 10001 # 注意初始化
        dp[0] = 0
        for i in coins:
            for j in range(i, amount + 1):
                dp[j] = min(dp[j], dp[j - i] + 1)
        if dp[amount] == float('inf'): return -1       
        return dp[amount]

279.完全平方数

力扣题目链接(opens new window)

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

示例 1:

  • 输入:n = 12
  • 输出:3
  • 解释:12 = 4 + 4 + 4

示例 2:

  • 输入:n = 13
  • 输出:2
  • 解释:13 = 4 + 9

提示:

  • 1 <= n <= 10^4

类似上一题,但要自己构造nums数组

class Solution:
    def numSquares(self, n: int) -> int:
        nums = [i**2 for i in range(1, n + 1) if i**2 <= n]
        dp = [float('inf')] * 10001
        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[n] if dp[n] !=  float('inf') else 0 

139.单词拆分

力扣题目链接(opens new window)

给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

说明:

拆分时可以重复使用字典中的单词。

你可以假设字典中没有重复的单词。

示例 1:

  • 输入: s = “leetcode”, wordDict = [“leet”, “code”]
  • 输出: true
  • 解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。

示例 2:

  • 输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
  • 输出: true
  • 解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。
  • 注意你可以重复使用字典中的单词。

示例 3:

  • 输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
  • 输出: false

回溯分割问题-超时-copy

class Solution {
private:
    bool backtracking (const string& s, const unordered_set<string>& wordSet, int startIndex) {
        if (startIndex >= s.size()) {
            return true;
        }
        for (int i = startIndex; i < s.size(); i++) {
            string word = s.substr(startIndex, i - startIndex + 1);
            if (wordSet.find(word) != wordSet.end() && backtracking(s, wordSet, i + 1)) {
                return true;
            }
        }
        return false;
    }
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
        return backtracking(s, wordSet, 0);
    }
};
  • 时间复杂度:O(2^n),因为每一个单词都有两个状态,切割和不切割
  • 空间复杂度:O(n),算法递归系统调用栈的空间

记忆化递归优化-copy

使用memory数组保存每次计算的以startIndex起始的计算结果,如果memory[startIndex]里已经被赋值了,直接用memory[startIndex]的结果

class Solution {
private:
    bool backtracking (const string& s,
            const unordered_set<string>& wordSet,
            vector<bool>& memory,
            int startIndex) {
        if (startIndex >= s.size()) {
            return true;
        }
        // 如果memory[startIndex]不是初始值了,直接使用memory[startIndex]的结果
        if (!memory[startIndex]) return memory[startIndex];
        for (int i = startIndex; i < s.size(); i++) {
            string word = s.substr(startIndex, i - startIndex + 1);
            if (wordSet.find(word) != wordSet.end() && backtracking(s, wordSet, memory, i + 1)) {
                return true;
            }
        }
        memory[startIndex] = false; // 记录以startIndex开始的子串是不可以被拆分的
        return false;
    }
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
        vector<bool> memory(s.size(), 1); // -1 表示初始化状态
        return backtracking(s, wordSet, memory, 0);
    }
};

这个时间复杂度其实也是:O(2^n)

完全背包问题

  1. 确定dp数组(dp table)以及下标的含义:dp[i] : 字符串长度为i,dp[i]为true,表示可以拆分为一个或多个单词

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

  3. dp数组如何初始化:dp[0]=true

  4. 确定遍历顺序:本题为排列问题,先遍历背包,再遍历物品

  5. 举例推导dp数组:输入: s = “leetcode”, wordDict = [“leet”, “code”]为例,dp状态如图:

    139.单词拆分
  • 时间复杂度:O(n^3),因为substr返回子串的副本是O(n)的复杂度(这里的n是substring的长度)
  • 空间复杂度:O(n)
class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        dp = [False] * (len(s)+1)  # dp[j]代表s[:j]是否能拆分
        dp[0] = True
        for j in range(1, len(s)+1): # 排列问题,先遍历背包
            for word in wordDict:
                if j >= len(word):
                    dp[j] = dp[j] or (dp[j - len(word)] and s[(j-len(word)):j] == word)
                print(dp)
        return dp[j]

背包问题总结篇

年前我们已经把背包问题都讲完了,那么现在我们要对背包问题进行总结一番。

背包问题是动态规划里的非常重要的一部分,所以我把背包问题单独总结一下,等动态规划专题更新完之后,我们还会在整体总结一波动态规划。

关于这几种常见的背包,其关系如下:

416.分割等和子集1

通过这个图,可以很清晰分清这几种常见背包之间的关系。

在讲解背包问题的时候,我们都是按照如下五部来逐步分析,相信大家也体会到,把这五部都搞透了,算是对动规来理解深入了。

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

其实这五部里哪一步都很关键,但确定递推公式和确定遍历顺序都具有规律性和代表性,所以下面我从这两点来对背包问题做一做总结

#背包递推公式

问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); ,对应题目如下:

  • 动态规划:416.分割等和子集(opens new window)
  • 动态规划:1049.最后一块石头的重量 II(opens new window)

问装满背包有几种方法:dp[j] += dp[j - nums[i]] ,对应题目如下:

  • 动态规划:494.目标和(opens new window)
  • 动态规划:518. 零钱兑换 II(opens new window)
  • 动态规划:377.组合总和Ⅳ(opens new window)
  • 动态规划:70. 爬楼梯进阶版(完全背包)(opens new window)

问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); ,对应题目如下:

  • 动态规划:474.一和零(opens new window)

问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); ,对应题目如下:

  • 动态规划:322.零钱兑换(opens new window)
  • 动态规划:279.完全平方数(opens new window)

#遍历顺序

#01背包

在动态规划:关于01背包问题,你该了解这些! (opens new window)中我们讲解二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。

和动态规划:关于01背包问题,你该了解这些!(滚动数组) (opens new window)中,我们讲解一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。

一维dp数组的背包在遍历顺序上和二维dp数组实现的01背包其实是有很大差异的,大家需要注意!

#完全背包

说完01背包,再看看完全背包。

在动态规划:关于完全背包,你该了解这些! (opens new window)中,讲解了纯完全背包的一维dp数组实现,先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。

但是仅仅是纯完全背包的遍历顺序是这样的,题目稍有变化,两个for循环的先后顺序就不一样了。

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

相关题目如下:

  • 求组合数:动态规划:518.零钱兑换II(opens new window)
  • 求排列数:动态规划:377. 组合总和 Ⅳ (opens new window)、动态规划:70. 爬楼梯进阶版(完全背包)(opens new window)

如果求最小数,那么两层for循环的先后顺序就无所谓了,相关题目如下:

  • 求最小数:动态规划:322. 零钱兑换 (opens new window)、动态规划:279.完全平方数(opens new window)

对于背包问题,其实递推公式算是容易的,难是难在遍历顺序上,如果把遍历顺序搞透,才算是真正理解了

#总结

这篇背包问题总结篇是对背包问题的高度概括,讲最关键的两部:递推公式和遍历顺序,结合力扣上的题目全都抽象出来了

而且每一个点,我都给出了对应的力扣题目

最后如果你想了解多重背包,可以看这篇动态规划:关于多重背包,你该了解这些! (opens new window),力扣上还没有多重背包的题目,也不是面试考察的重点。

如果把我本篇总结出来的内容都掌握的话,可以说对背包问题理解的就很深刻了,用来对付面试中的背包问题绰绰有余!

背包问题总结:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rA3fgglT-1679973227533)(https://code-thinking-1253855093.file.myqcloud.com/pics/%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%981.jpeg)]

198.打家劫舍

力扣题目链接(opens new window)

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

  • 示例 1:
  • 输入:[1,2,3,1]
  • 输出:4

解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。

  • 示例 2:
  • 输入:[2,7,9,3,1]
  • 输出:12 解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示:

  • 0 <= nums.length <= 100
  • 0 <= nums[i] <= 400

当前房屋偷与不偷取决于 前一个房屋和前两个房屋是否被偷了

  1. 确定dp数组(dp table)以及下标的含义:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]

  2. 确定递推公式:

    • 如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i] ,即:第i-1房一定是不考虑的,找出 下标i-2(包括i-2)以内的房屋,最多可以偷窃的金额为dp[i-2] 加上第i房间偷到的钱。
    • 如果不偷第i房间,那么dp[i] = dp[i - 1],即考 虑i-1房,(注意这里是考虑,并不是一定要偷i-1房,容易混淆的点
    • 然后dp[i]取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
  3. dp数组如何初始化:dp[0] = nums[0],dp[1] = max(nums[0], nums[1]);

  4. 确定遍历顺序:前到后遍历

  5. 举例推导dp数组:输入[2,7,9,3,1]为例。

    198.打家劫舍
    class Solution:
        def rob(self, nums: List[int]) -> int:
            if len(nums) == 0:return 0
            if len(nums) == 1:return nums[0]
            dp = [0] * len(nums)
            dp[0], dp[1] = nums[0], 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]
    
    class Solution:
        def rob(self, nums: List[int]) -> int:
            if len(nums) == 0:return 0
            if len(nums) == 1:return nums[0]
            a, b = nums[0],max(nums[0],nums[1])
            for i in range(2, len(nums)):
                tmp = b
                b,a = max(a + nums[i], tmp), tmp
                print(b)
            return b
    

213.打家劫舍II

力扣题目链接(opens new window)

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。

示例 1:

输入:nums = [2,3,2] 输出:3 解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2: 输入:nums = [1,2,3,1] 输出:4 解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。

示例 3: 输入:nums = [0] 输出:0

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 1000
  1. 确定dp数组(dp table)以及下标的含义:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]

  2. 确定递推公式:

    • 递推公式:dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
    • 比较考虑偷头节点与考虑偷尾节点的大小(头尾都不偷,包含在上面两种情况里面)
  3. dp数组如何初始化:dp[0] = nums[0],dp[1] = max(nums[0], nums[1]);

  4. 确定遍历顺序:前到后遍历

  5. 举例推导dp数组:输入[2,7,9,3,1]为例。

    class Solution:
        def rob(self, nums: List[int]) -> int:
            #一是不偷第一间房,二是不偷最后一间房
            if len(nums)==1:#题目中提示nums.length>=1,所以不需要考虑len(nums)==0的情况
                return nums[0]
            val1=self.roblist(nums[1:])#不偷第一间房
            val2=self.roblist(nums[:-1])#不偷最后一间房
            return max(val1,val2)
    
        def roblist(self,nums):
            dp=[0]*len(nums)
            dp[0]=nums[0]
            for i in range(1,len(nums)):
                if i==1:
                    dp[i]=max(dp[i-1],nums[i])
                else:
                    dp[i]=max(dp[i-1],dp[i-2]+nums[i])
            return dp[-1]
    

337.打家劫舍 III

力扣题目链接(opens new window)

在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。

337.打家劫舍III

dp树

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def rob(self, root: Optional[TreeNode]) -> int:
        # 使用动态规划 dp[(0,1),(),()],下标0记录当前最大,1记录下一个最大
        def travesal(node):
            if not node:# 终止条件,当前节点为0
                return (0, 0)
            left = travesal(node.left)
            right = travesal(node.right)
            val0 = max(left[0], left[1]) + max(right[0], right[1]) # 不偷当前
            val1 = node.val + left[0] + right[0] # 偷当前
            return (val0, val1)
        dp = travesal(root)
        return max(dp)

121. 买卖股票的最佳时机

力扣题目链接(opens new window)

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例 2:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0

  1. dp数组:dp[i] [0]表示第i天持有股票的最大现金,dp[i] [1]表示第i天不持有股票的最大现金
  2. 递推公式:
    1. 持有(第i-1天持有或第i天买入):dp[i] [0] = max(dp[i - 1] [0], -price[i])
    2. 不持有(第i-1天不持有或第i天卖出)dp[i] [1] = max(dp[i - 1] [1], price[i] - dp[i - 1] [0])
  3. 初始化:dp[0] [0] = -price[0], dp[0] [1] = 0
  4. 遍历顺序:顺序遍历
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        # 动态规划,dp[i][0] 表示第i天持有股票所得最多现金,dp[i][1]不持有
        dp = [[0] * 2 for _ in range(len(prices))] # 第i天买入的最大利
        dp[0][0], dp[0][1] = -prices[0], 0
        for i in range(1, len(prices)):
            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]

优化:

dp0 持有,dp1不持有

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        # 用两个状态优化,dp0持有,dp1不持有
        dp0, dp1 = -prices[0], 0
        for i in range(1, len(prices)):
            dp0 = max(dp0, -prices[i])
            dp1 = max(dp1, prices[i] + dp0)
        return dp1

贪心:

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        # 贪心
        low = float('inf')
        res = 0
        for i in range(len(prices)):
           low = min(prices[i], low) # 记住左边的最小值
            res = max(prices[i] - low, res)  # 记住最大结果
        return res

122.买卖股票的最佳时机II

力扣题目链接(opens new window)

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4。随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。

示例 2:
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

提示:

  • 1 <= prices.length <= 3 * 10 ^ 4
  • 0 <= prices[i] <= 10 ^ 4

dp0持有(具有dp1的原始积累),dp1不持有

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        # 贪心
        res = 0
        for i in range(1,len(prices)): # 寻找全局最优
            n = prices[i] - prices[i - 1] 
            res += n if n > 0 else 0 # 局部最优
        return res

        # 动态规划
        dp = [[0]*2 for i in range(len(prices))]  # dp[i]代表第i天的最大利益
        dp[0][0],dp[0][1] =-prices[0], 0
        for i in range(1,len(prices)):
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i])
            dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0])
        return dp[-1][1]
        
        # # 两个状态转移
        dp0, dp1 = -prices[0], 0
        for i in range(1, len(prices)):
            dp0 = max(dp0, dp1-prices[i])  # 唯一不同之处
            dp1 = max(dp1, prices[i] + dp0)
        return dp1

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

力扣题目链接(opens new window)

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1: 输入:prices = [3,3,5,0,0,3,1,4] 输出:6 解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3。

示例 2: 输入:prices = [1,2,3,4,5] 输出:4 解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4。注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3: 输入:prices = [7,6,4,3,1] 输出:0 解释:在这个情况下, 没有交易完成, 所以最大利润为0。

示例 4: 输入:prices = [1] 输出:0

提示:

  • 1 <= prices.length <= 10^5
  • 0 <= prices[i] <= 10^5

关键在于至多买卖两次,这意味着可以买卖一次,可以买卖两次,也可以不买卖

dp1第一次持有,dp2第一次不持有,dp3第二次持有,dp4第二次不持有

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        dp1 = dp3 = -prices[0]
        dp2 = dp4 = 0
        for i in range(1, len(prices)):
            dp1 = max(dp1, -prices[i])
            dp2 = max(dp2, dp1 + prices[i])
            dp3 = max(dp3, dp2 - prices[i])
            dp4 = max(dp4, dp3 + prices[i])
        return dp4

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

力扣题目链接(opens new window)

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1: 输入:k = 2, prices = [2,4,1] 输出:2 解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2。

示例 2: 输入:k = 2, prices = [3,2,6,5,0,3] 输出:7 解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4。随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。

提示:

  • 0 <= k <= 100
  • 0 <= prices.length <= 1000
  • 0 <= prices[i] <= 1000
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(word1)+1):
            dp[i][0] = i
        for j in range(len(word2)+1):
            dp[0][j] = j
        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:
                    dp[i][j] = min(dp[i-1][j-1] + 2, dp[i-1][j] + 1, dp[i][j-1] + 1)
        return dp[-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(word1)+1):
            dp[i][0] = i
        for j in range(len(word2)+1):
            dp[0][j] = j
        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:
                    dp[i][j] = min(dp[i-1][j-1] + 2, dp[i-1][j] + 1, dp[i][j-1] + 1)
        return dp[-1][-1]

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

力扣题目链接(opens new window)

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

  • 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

示例:

  • 输入: [1,2,3,0,2]
  • 输出: 3
  • 解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
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(word1)+1):
            dp[i][0] = i
        for j in range(len(word2)+1):
            dp[0][j] = j
        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:
                    dp[i][j] = min(dp[i-1][j-1] + 2, dp[i-1][j] + 1, dp[i][j-1] + 1)
        return dp[-1][-1]

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

力扣题目链接(opens new window)

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

返回获得利润的最大值。

注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

示例 1:

  • 输入: prices = [1, 3, 2, 8, 4, 9], fee = 2
  • 输出: 8

解释: 能够达到的最大利润:

  • 在此处买入 prices[0] = 1
  • 在此处卖出 prices[3] = 8
  • 在此处买入 prices[4] = 4
  • 在此处卖出 prices[5] = 9
  • 总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
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(word1)+1):
            dp[i][0] = i
        for j in range(len(word2)+1):
            dp[0][j] = j
        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:
                    dp[i][j] = min(dp[i-1][j-1] + 2, dp[i-1][j] + 1, dp[i][j-1] + 1)
        return dp[-1][-1]

300.最长递增子序列

力扣题目链接(opens new window)

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

  • 输入:nums = [10,9,2,5,3,7,101,18]
  • 输出:4
  • 解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

  • 输入:nums = [0,1,0,3,2,3]
  • 输出:4
class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        # dp[i] 代表下标为i的最长递增子序列长度
        dp = [1] * len(nums)
        for i in range(1, len(nums)):
            for j in range(i):
                if nums[i] > nums[j]:
                    dp[i] = max(dp[i], dp[j] + 1)
        return max(dp)  

674. 最长连续递增序列

力扣题目链接(opens new window)

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。

连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], …, nums[r - 1], nums[r]] 就是连续递增子序列。

示例 1:

  • 输入:nums = [1,3,5,4,7]
  • 输出:3
  • 解释:最长连续递增序列是 [1,3,5], 长度为3。尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
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] + 1
        # return max(dp)
        res = dp0 = dp1 = 1
        for i in range(1, len(nums)):
            if nums[i] > nums[i - 1]:
                dp1 = dp0 + 1
                dp0 = dp1
            else:dp0 = 1
            if dp1 > res:res = dp1  
        return res

718. 最长重复子数组

力扣题目链接(opens new window)

给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。

示例:

输入:

  • A: [1,2,3,2,1]
  • B: [3,2,1,4,7]
  • 输出:3
  • 解释:长度最长的公共子数组是 [3, 2, 1] 。
class Solution:
    def findLength(self, nums1: List[int], nums2: List[int]) -> int:
        # # # 暴力法,超时
        # res = 0
        # for i in range(len(nums1)):
        #     for j in range(len(nums2)):
        #         if nums1[i] == nums2[j]: 
        #             k = 0
        #             while i+k < len(nums1) and j+k < len(nums2) and nums1[i+k] == nums2[j+k]:
        #                 k += 1
        #             res = max(res, k)                
        # return res

        # # 动态规划
        # res = 0
        # 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] + 1
        #         res = max(res, dp[i][j])
        # return res

        # 滚动数组
        res = 0
        dp = [0] * (len(nums2) + 1)
        for i in range(1, len(nums1) + 1):
            for j in range(len(nums2), 0, -1):
                if nums1[i - 1] == nums2[j - 1]:
                    dp[j] = dp[j - 1] + 1
                else:
                    dp[j] = 0
                res = max(res, dp[j])
        return res

1143.最长公共子序列

力扣题目链接(opens new window)

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

若这两个字符串没有公共子序列,则返回 0。

示例 1:

输入:text1 = “abcde”, text2 = “ace” 输出:3 解释:最长公共子序列是 “ace”,它的长度为 3。

示例 2: 输入:text1 = “abc”, text2 = “abc” 输出:3 解释:最长公共子序列是 “abc”,它的长度为 3。

示例 3: 输入:text1 = “abc”, text2 = “def” 输出:0 解释:两个字符串没有公共子序列,返回 0。

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        len1, len2 = len(text1)+1, len(text2)+1
        dp = [[0 for _ in range(len1)] for _ in range(len2)] # 先对dp数组做初始化操作
        for i in range(1, len2):
            for j in range(1, len1): # 开始列出状态转移方程
                if text1[j-1] == text2[i-1]:
                    dp[i][j] = dp[i-1][j-1] + 1
                else:
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1])
        return dp[-1][-1]

1035.不相交的线

力扣题目链接(opens new window)

我们在两条独立的水平线上按给定的顺序写下 A 和 B 中的整数。

现在,我们可以绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且我们绘制的直线不与任何其他连线(非水平线)相交。

以这种方法绘制线条,并返回我们可以绘制的最大连线数。

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] + 1
                else:
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
        return dp[-1][-1]

53. 最大子序和

力扣题目链接(opens new window)

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例: 输入: [-2,1,-3,4,-1,2,1,-5,4] 输出: 6 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6

贪心解法

动规解法:

  • dp数组:dp[i]代表下标为i 的最大子序列和
class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        # 贪心法
        # res = -float('inf')
        # count = 0
        # for i in range(len(nums)):
        #     count += nums[i]
        #     if count > res:
        #         res = count
        #     if count < 0:
        #         count = 0
        # return res

        # 动态规划
        dp = [0] * len(nums) # dp[i]代表下标为 i 的最大子数组和
        dp[0] = nums[0]
        for i in range(1, len(nums)):
            dp[i] = max(nums[i], dp[i - 1] + nums[i])
        return max(dp)

392.判断子序列

力扣题目链接(opens new window)

给定字符串 s 和 t ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。

示例 1: 输入:s = “abc”, t = “ahbgdc” 输出:true

示例 2: 输入:s = “axc”, t = “ahbgdc” 输出:false

提示:

  • 0 <= s.length <= 100
  • 0 <= t.length <= 10^4

两个字符串都只由小写字符组成。

class Solution:
    def isSubsequence(self, s: str, t: str) -> bool:
        # # 双指针
        # i = j = 0
        # while i < len(s) and j < len(t):
        #     if s[i] == t[j]:
        #         i += 1
        #     j += 1
        # return i == len(s)

        # 动态规划
        # dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]
        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] + 1
                else:
                    dp[i][j] = dp[i][j - 1]
        return dp[-1][-1] == len(s)

115.不同的子序列

力扣题目链接(opens new window)

给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。

字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,“ACE” 是 “ABCDE” 的一个子序列,而 “AEC” 不是)

题目数据保证答案符合 32 位带符号整数范围。

class Solution:
    def numDistinct(self, s: str, t: str) -> int:
        # dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]
        dp = [[0] * (len(t)+1) for _ in range(len(s)+1)]
        for i in range(len(s)):# 初始化
            dp[i][0] = 1 
        for i in range(1, len(s)+1):
            for j in range(1, len(t)+1):
                if s[i - 1] == t[j - 1]:  # 使用s[i-1]+不使用s[i-1]
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]
                else:  # 不使用s[i-1]匹配
                    dp[i][j] = dp[i - 1][j]
        return dp[-1][-1]

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

力扣题目链接(opens new window)

给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。

示例:

  • 输入: “sea”, “eat”
  • 输出: 2
  • 解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"
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(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] + 1
                else:
                    dp[i][j] = max(dp[i][j - 1], dp[i - 1][j])
        return len(word1)+len(word2)-2*dp[-1][-1]

72. 编辑距离

力扣题目链接(opens new window)

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符
  • 示例 1:
  • 输入:word1 = “horse”, word2 = “ros”
  • 输出:3
  • 解释: horse -> rorse (将 ‘h’ 替换为 ‘r’) rorse -> rose (删除 ‘r’) rose -> ros (删除 ‘e’)
class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        # dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离
        dp = [[0] * (len(word2)+1) for _ in range(len(word1)+1)]
        for i in range(len(word1)+1): dp[i][0] = i
        for j in range(len(word2)+1): dp[0][j] = j
        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:
                    dp[i][j] = min(dp[i-1][j-1], dp[i][j-1],dp[i-1][j])+1
        return dp[-1][-1]

647. 回文子串

力扣题目链接(opens new window)

给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例 1:

  • 输入:“abc”
  • 输出:3
  • 解释:三个回文子串: “a”, “b”, “c”

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

class Solution:
    def countSubstrings(self, s: str) -> int:
        n = len(s)
        # dp[i][j] 表示 s[i:j] 是否是回文串
        dp = [[False] * n for _ in range(n)]
        res = 0
        # 递推开始
        for i in range(len(s)-1, -1, -1): #注意遍历顺序
            for j in range(i, len(s)):
                if s[i] == s[j] and (j-i<=1 or dp[i+1][j-1]):
                    res += 1
                    dp[i][j] = True  
        return res

516.最长回文子序列

力扣题目链接(opens new window)

给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。

示例 1: 输入: “bbbab” 输出: 4 一个可能的最长回文子序列为 “bbbb”。

示例 2: 输入:“cbbd” 输出: 2 一个可能的最长回文子序列为 “bb”。

dp[i] [j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i] [j]

class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        # dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]。
        dp = [[0] * len(s) for _ in range(len(s))]
        for 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] + 2
                else:
                    dp[i][j] = max(dp[i+1][j], dp[i][j-1])
        return dp[0][-1]

5.最长回文子串

5. 最长回文子串 - 力扣(Leetcode)

给你一个字符串 s,找到 s 中最长的回文子串。

如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。给你一个字符串 s,找到 s 中最长的回文子串。

如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。

示例 1:

输入:s = "babad"
输出:"bab"
解释:"aba" 同样是class Solution:

示例 2:

输入:s = "cbbd"
输出:"bb"

动态规划

class Solution:
    def longestPalindrome(self, s: str) -> str:
        n = len(s)
        if n < 2: return s
        max_len = 1
        begin = 0
        dp = [[False] * n for _ in range(n)]# dp[i][j] 表示 s[i..j] 是否是回文串
        for 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] = False 
                else:
                    if j - i < 3: 
                        dp[i][j] = True
                    else: 
                        dp[i][j] = dp[i + 1][j - 1]
                if dp[i][j] and j - i + 1 > max_len:# 记录回文长度和起始位置
                    max_len = j - i + 1
                    begin = i
        return s[begin:begin + max_len]

中心扩散

class Solution:
    def expandAroundCenter(self, s, left, right):
        while left >= 0 and right < len(s) and s[left] == s[right]:
            left -= 1
            right += 1
        return left + 1, right - 1

    def longestPalindrome(self, s: str) -> str:
        start, end = 0, 0
        for i in range(len(s)):
            left1, right1 = self.expandAroundCenter(s, i, i)
            left2, right2 = self.expandAroundCenter(s, i, i + 1)
            if right1 - left1 > end - start:
                start, end = left1, right1
            if right2 - left2 > end - start:
                start, end = left2, right2
        return s[start: end + 1]

你可能感兴趣的:(代码随想录刷题笔记,动态规划,算法,leetcode)