动态规划,英文: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])。
但如果是贪心呢,每次拿物品选一个最大的或者最小的就完事了,和上一个状态没有关系
找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的!
写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍
先思考这三个问题:
力扣题目链接(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:
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)
力扣题目链接(opens new window)
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 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
力扣题目链接(opens new window)
旧题目描述:
数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。
每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。
请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。
示例 1:
示例 2:
提示:
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)
力扣题目链接(opens new window)
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
示例 2:
解释: 从左上角开始,总共有 3 条路径可以到达右下角。
提示:
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))
力扣题目链接(opens new window)
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 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]
力扣题目链接(opens new window)
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
示例 1:
示例 2:
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]
力扣题目链接(opens new window)
给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?
示例:
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]
背包问题,大家都知道,有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
背包问题有多种背包方式,常见的有:01背包、完全背包、多重背包、分组背包和混合背包等等。
要注意题目描述中商品是不是可以重复放入。
即一个商品如果可以重复多次放入是完全背包,而只能放入一次是01背包
背包最大重量为4。
物品为:
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
问背包能背的物品最大价值是多少?
以下讲解和图示中出现的数字都是以这个例子为例。
依然动规五部曲分析一波。
对于背包问题,有一种写法, 是使用二维数组,即dp[ i ] [ j ] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
切记dp数组的含义!
所以递归公式: 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]);
力扣题目链接(opens new window)
题目难易:中等
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意: 每个数组中的元素不会超过 100 数组的大小不会超过 200
示例 1:
示例 2:
提示:
只有确定了如下四点,才能把01背包问题套到本题上来。
dp[j]表示 背包总容量(所能装的总重量)是j,放进物品后,背的最大重量为dp[j]。
当 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
力扣题目链接(opens new window)
题目难度:中等
有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。
示例:
解释:
提示:
转化为分割子集问题。思路与上一题类似
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]);
确定dp数组以及下标的含义:dp[i] [j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
确定递推公式:dp[i] [j] = max(dp[i - 1] [j], dp[i - 1] [j - weight[i]] + value[i]);
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];
}
// 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]);
}
}
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
最终结果就是dp[2] [4]。
分析一下和二维dp数组有什么区别,在初始化和遍历顺序上又有什么差异?
01背包一维数组分析如下:
确定dp数组的定义:在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。
一维dp数组的递推公式
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
一维dp数组如何初始化:如果物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。
一维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]);
}
}
力扣题目链接(opens new window)
难度:中等
给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
示例:
解释:
一共有5种方法让最终目标和为3。
提示:
会超时,类似组合总和,略
假设加法的总和为x,那么减法对应的总和就是sum - x。
所以我们要求的是 x - (sum - x) = target
x = (target + sum) / 2
问题就转化:装满容量为x的背包,有几种方法
之前都是求容量为j的背包,最多能装多少。本题则是装满有几种方法。其实这就是一个组合问题了
dp下标含义:dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法
递推公式:只要搞到nums[i]),凑成dp[j]就有dp[j - nums[i]] 种方法,dp[j] = dp[j] + dp[j - nums[i]], 这个公式在后面在讲解背包解决排列组合问题的时候还会用到!例如:dp[j],j 为5,
初始化dp:dp[0] = 1
遍历顺序:01背包问题一维dp,nums放在外循环,target在内循环,且内循环倒序
举例推导:输入:nums: [1, 1, 1, 1, 1], S= 3。bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4
dp数组状态变化如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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为背包容量
组合问题求个数可以用动态规划,但是列出所有组合只能用回溯
力扣题目链接(opens new window)
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例 1:
示例 2:
提示:
本题中strs 数组里的元素就是物品,每个物品都是一个!,而m 和 n相当于是一个背包,两个维度的背包
本题是求 给定背包容量,装满背包最多有多少个物品
动规五部曲:
确定dp数组以及下标的含义:最多有i个0和j个1的strs的最大子集的个数为dp[i] [j]
确定递推公式: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
dp数组如何初始化:dp = [[0]*(n+1) for _ in range(m+1)]
确定遍历顺序:先物品str,后容量(倒序遍历)
举例推导:以输入:[“10”,“0001”,“111001”,“1”,“0”],m = 3,n = 3为例,最后dp数组的状态如下所示:
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]
可以重复出现,两次for都是从前往后遍历
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
力扣题目链接(opens new window)
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 1:
解释: 有四种方式可以凑成总金额:
注意,你可以假设:
完全背包从前往后遍历
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]
力扣题目链接(opens new window)
难度:中等
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
示例:
所有可能的组合为: (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]
力扣题目链接(opens new window)
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1: 输入: 2 输出: 2 解释: 有两种方法可以爬到楼顶。
示例 2: 输入: 3 输出: 3 解释: 有三种方法可以爬到楼顶。
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]
力扣题目链接(opens new window)
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
示例 1:
示例 2:
提示:
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]
力扣题目链接(opens new window)
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
示例 2:
提示:
类似上一题,但要自己构造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
力扣题目链接(opens new window)
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例 1:
示例 2:
示例 3:
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);
}
};
使用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)
确定dp数组(dp table)以及下标的含义:dp[i] : 字符串长度为i,dp[i]为true,表示可以拆分为一个或多个单词
确定递推公式: 如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true
dp数组如何初始化:dp[0]=true
确定遍历顺序:本题为排列问题,先遍历背包,再遍历物品
举例推导dp数组:输入: s = “leetcode”, wordDict = [“leet”, “code”]为例,dp状态如图:
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]
年前我们已经把背包问题都讲完了,那么现在我们要对背包问题进行总结一番。
背包问题是动态规划里的非常重要的一部分,所以我把背包问题单独总结一下,等动态规划专题更新完之后,我们还会在整体总结一波动态规划。
关于这几种常见的背包,其关系如下:
通过这个图,可以很清晰分清这几种常见背包之间的关系。
在讲解背包问题的时候,我们都是按照如下五部来逐步分析,相信大家也体会到,把这五部都搞透了,算是对动规来理解深入了。
其实这五部里哪一步都很关键,但确定递推公式和确定遍历顺序都具有规律性和代表性,所以下面我从这两点来对背包问题做一做总结。
问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); ,对应题目如下:
问装满背包有几种方法:dp[j] += dp[j - nums[i]] ,对应题目如下:
问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); ,对应题目如下:
问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); ,对应题目如下:
在动态规划:关于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循环遍历物品。
相关题目如下:
如果求最小数,那么两层for循环的先后顺序就无所谓了,相关题目如下:
对于背包问题,其实递推公式算是容易的,难是难在遍历顺序上,如果把遍历顺序搞透,才算是真正理解了。
这篇背包问题总结篇是对背包问题的高度概括,讲最关键的两部:递推公式和遍历顺序,结合力扣上的题目全都抽象出来了。
而且每一个点,我都给出了对应的力扣题目。
最后如果你想了解多重背包,可以看这篇动态规划:关于多重背包,你该了解这些! (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)]
力扣题目链接(opens new window)
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。
提示:
当前房屋偷与不偷取决于 前一个房屋和前两个房屋是否被偷了
确定dp数组(dp table)以及下标的含义:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]
确定递推公式:
dp数组如何初始化:dp[0] = nums[0],dp[1] = max(nums[0], nums[1]);
确定遍历顺序:前到后遍历
举例推导dp数组:输入[2,7,9,3,1]为例。
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
力扣题目链接(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
提示:
确定dp数组(dp table)以及下标的含义:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]
确定递推公式:
dp数组如何初始化:dp[0] = nums[0],dp[1] = max(nums[0], nums[1]);
确定遍历顺序:前到后遍历
举例推导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]
力扣题目链接(opens new window)
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
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)
力扣题目链接(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
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
力扣题目链接(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。
提示:
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
力扣题目链接(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
提示:
关键在于至多买卖两次,这意味着可以买卖一次,可以买卖两次,也可以不买卖
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
力扣题目链接(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 。
提示:
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]
力扣题目链接(opens new window)
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
示例:
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]
力扣题目链接(opens new window)
给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
示例 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]
力扣题目链接(opens new window)
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
示例 2:
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)
力扣题目链接(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:
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
力扣题目链接(opens new window)
给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。
示例:
输入:
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
力扣题目链接(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]
力扣题目链接(opens new window)
我们在两条独立的水平线上按给定的顺序写下 A 和 B 中的整数。
现在,我们可以绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且我们绘制的直线不与任何其他连线(非水平线)相交。
以这种方法绘制线条,并返回我们可以绘制的最大连线数。
等同于最长公共子序列问题
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]
力扣题目链接(opens new window)
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例: 输入: [-2,1,-3,4,-1,2,1,-5,4] 输出: 6 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6
贪心解法
动规解法:
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)
力扣题目链接(opens new window)
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
示例 1: 输入:s = “abc”, t = “ahbgdc” 输出:true
示例 2: 输入:s = “axc”, t = “ahbgdc” 输出:false
提示:
两个字符串都只由小写字符组成。
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)
力扣题目链接(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]
力扣题目链接(opens new window)
给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。
示例:
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]
力扣题目链接(opens new window)
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
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]
力扣题目链接(opens new window)
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例 1:
下到上,从左到右遍历,这样保证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
力扣题目链接(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. 最长回文子串 - 力扣(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]