提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
一、动态规划
动态规划的三要素:最优子结构,边界和状态转移函数,
- 最优子结构是指每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到(子问题的最优解能够决定这个问题的最优解)
- 边界指的是问题最小子集的解(初始范围)
- 状态转移函数是指从一个阶段向另一个阶段过度的具体形式,描述的是两个相邻子问题之间的关系(递推式)
重叠子问题,对每个子问题只计算一次,然后将其计算的结果保存到一个表格中,每一次需要上一个子问题解时,进行调用,只要o(1)时间复杂度,准确的说,动态规划是利用空间去换取时间的算法.
判断是否可以利用动态规划求解,第一个是判断是否存在重叠子问题,
二、问题
1.易
爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。 - 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
分析
假定n=10,首先考虑最后一步的情况,要么从第九级台阶再走一级到第十级,要么从第八级台阶走两级到第十级,因而,要想到达第十级台阶,最后一步一定是从第八级或者第九级台阶开始.也就是说已知从地面到第八级台阶一共有X种走法,从地面到第九级台阶一共有Y种走法,那么从地面到第十级台阶一共有X+Y种走法.
即
F
(
10
)
=
F
(
9
)
+
F
(
8
)
F(10)=F(9)+F(8)
F(10)=F(9)+F(8)
分析到这里,动态规划的三要素出来了.
- 边界:F(1)=1,F(2)=2
- 最优子结构:F(10)的最优子结构即F(9)和F(8)
- 状态转移函数:F(n)=F(n-1)+F(n-2)
class Solution:
def climbStairs(self, n: int) -> int:
if n<=2:
return n
a=1 # 边界条件
b=2 # 边界条件
temp=0
for i in range(3,n+1):
temp=a+b # 状态转移
a=b # 最优子结构
b=temp # 最优子结构
return temp
复杂度分析
- 时间复杂度:循环执行 n 次,每次花费常数的时间代价,故渐进时间复杂度为 O ( n ) O(n) O(n)。
- 空间复杂度:这里只用了常数个变量作为辅助空间,故渐进空间复杂度为 O ( 1 ) O(1) O(1)。
121. 买卖股票的最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
class Solution:
def maxProfit(self, prices: List[int]) -> int:
'''
思路: 从最后一个点4分析,如果4要获得最大利润,那么要获取其前面最小值,才能获取最大利润
即可以得到递推式为:f(4)=max(f(6),4-最小金额)
动态规划:
边界点:f(1)=0
最优子结构:f(4)的为f(6)
状态转移方程:f(4)=max(f(6),4-最小金额)
'''
min_value=prices[0]
max_profit=[0 for i in prices]
for i in range(1,len(prices)):
if prices[i]<min_value:
min_value=prices[i]
max_profit[i]=max(max_profit[i-1],prices[i]-min_value)
return max_profit[-1]
复杂度分析
- 时间复杂度:时间复杂度为 O ( n ) O(n) O(n)。
- 空间复杂度:空间复杂度为 O ( n ) O(n) O(n)。
面试题 16.17. 连续数列
给定一个整数数组,找出总和最大的连续数列,并返回总和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
# 动态规划,利用数据存储每一时刻的最大值
# 初始条件:a[0]=nums[0]
# 最优子条件:a[i] a[i-1] a[i-1]
# 状态转移函数a[i] = max(a[i-1]+nums[i], a[i-1])
a=[0]*len(nums)
a[0]=nums[0]
for i in range(1,len(nums)):
a[i]=max(a[i-1]+nums[i],nums[i])
return max(a)
复杂度分析
- 时间复杂度:时间复杂度为 O ( n ) O(n) O(n)。
- 空间复杂度:空间复杂度为 O ( n ) O(n) O(n)。
1137. 第 N 个泰波那契数
泰波那契序列 Tn 定义如下:
T0 = 0, T1 = 1, T2 = 1, 且在 n >= 0 的条件下 Tn+3 = Tn + Tn+1 + Tn+2
给你整数 n,请返回第 n 个泰波那契数 Tn 的值。
示例 1:
输入:n = 4
输出:4
解释:
T_3 = 0 + 1 + 1 = 2
T_4 = 1 + 1 + 2 = 4
class Solution:
def tribonacci(self, n: int) -> int:
# 思路: 如题课题,动态规划问题,当前解与上一个解存在重叠部分,重叠子问题
# 状态如下:
# 边界 0 1 1
# 最优子状态 f(n+3)的最优子结构为f(n)+f(n+1)+f(n+2)
# 状态转移函数 f(n+3)=f(n)+f(n+1)+f(n+2)
a0=0 # 边界
a1=1
a2=1
cns=0
if n==0:
return 0
if n==1 or n==2:
return 1
for i in range(n-2):
cns=a0+a1+a2 # 转台转移
a0,a1,a2=a1,a2,cns
return cns
复杂度分析
- 时间复杂度:时间复杂度为 O ( n ) O(n) O(n)。
- 空间复杂度:空间复杂度为 O ( 1 ) O(1) O(1)。
剑指 Offer II 088. 爬楼梯的最少成本
数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。
每当爬上一个阶梯都要花费对应的体力值,一旦支付了相应的体力值,就可以选择向上爬一个阶梯或者爬两个阶梯。
请找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。
示例 1:
输入:cost = [10, 15, 20]
输出:15
解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15 。
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
# 存在重叠子问题,动态规划
# 边界 a[0]=0] a[1]=0
# 状态转移:a[10]=min(cost[8]+a[8],a[9]+cost[9])
min_cost=[0]*(len(cost)+1)
min_cost[0]=0
min_cost[1]=0
for i in range(2,len(cost)+1):
min_cost[i]=min(cost[i-1]+min_cost[i-1],min_cost[i-2]+cost[i-2])
return min_cost[len(cost)]
复杂度分析
- 时间复杂度:时间复杂度为 O ( n ) O(n) O(n)。
- 空间复杂度:空间复杂度为 O ( n ) O(n) O(n)。
1646. 获取生成数组中的最大值
给你一个整数 n 。按下述规则生成一个长度为 n + 1 的数组 nums :
nums[0] = 0
nums[1] = 1
当 2 <= 2 * i <= n 时,nums[2 * i] = nums[i]
当 2 <= 2 * i + 1 <= n 时,nums[2 * i + 1] = nums[i] + nums[i + 1]
返回生成数组 nums 中的 最大 值。
示例 1:
输入:n = 7
输出:3
解释:根据规则:
nums[0] = 0
nums[1] = 1
nums[(1 * 2) = 2] = nums[1] = 1
nums[(1 * 2) + 1 = 3] = nums[1] + nums[2] = 1 + 1 = 2
nums[(2 * 2) = 4] = nums[2] = 1
nums[(2 * 2) + 1 = 5] = nums[2] + nums[3] = 1 + 2 = 3
nums[(3 * 2) = 6] = nums[3] = 2
nums[(3 * 2) + 1 = 7] = nums[3] + nums[4] = 2 + 1 = 3
因此,nums = [0,1,1,2,1,3,2,3],最大值 3
class Solution:
def getMaximumGenerated(self, n: int) -> int:
# 动态规划题,判断条件为:存在重叠子问题
# 边界:a[0]=0,a[1]=1
# 最优子结构:a[n] a[n//2] n%2*(a[n//2+1])
# 状态转移函数:nums[i]=nums[i//2]+i%2*(nums[i//2+1])
if n<2:
return n
nums=[0]*(n+1)
nums[1]=1
for i in range(2,n+1):
nums[i]=nums[i//2]+i%2*(nums[i//2+1])
return max(nums)
复杂度分析
- 时间复杂度:时间复杂度为 O ( n ) O(n) O(n)。
- 空间复杂度:空间复杂度为 O ( n ) O(n) O(n)。
剑指 Offer 10- I. 斐波那契数列
写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:
F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 1:
输入:n = 2
输出:1
class Solution:
def fib(self, n: int) -> int:
if n==0:
return 0
a=[0]*(n+1)
a[1]=1
for i in range(2,n+1):
a[i]=a[i-1]+a[i-2]
return a[-1]%1000000007
复杂度分析
- 时间复杂度:时间复杂度为 O ( n ) O(n) O(n)。
- 空间复杂度:空间复杂度为 O ( n ) O(n) O(n)。
2.中
62. 不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
输入:m = 3, n = 7
输出:28
示例 2:
输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
- 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右
- 向下 -> 向右 -> 向下
分析
和爬楼图一个思路,首先考虑最后一个点的不同路径的情况,有两种,要么上一个节点,往下或者往右(i为行,j为列)
即状态为:
F
(
i
,
j
)
=
F
(
i
,
j
−
1
)
+
F
(
i
−
1
,
j
)
F(i,j)=F(i,j-1)+F(i-1,j)
F(i,j)=F(i,j−1)+F(i−1,j)
即三要素为
- 边界: F ( 0 , 0 ) = 1 F(0,0)=1 F(0,0)=1
- 最优子结构:F(3,4)的最优子结构即 F ( 2 , 4 ) F(2,4) F(2,4)(往下)和 F ( 3 , 3 ) F(3,3) F(3,3)(往右)
- 状态转移函数: F ( i , j ) = F ( i , j − 1 ) + F ( i − 1 , j ) F(i,j)=F(i,j-1)+F(i-1,j) F(i,j)=F(i,j−1)+F(i−1,j)
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
# 问题描述:只能向下或者向右走一步,那么finish(m,n)的最优解为 sum(dp[m-1,n]+dp[m,n-1])
# 初始值为 dp[0,0]=1
dp=[[1 for i in range(n)] for j in range(m)]
for i in range(m):
for j in range(n):
if i==0 or j==0:
continue
else:
dp[i][j]=dp[i-1][j]+dp[i][j-1]
return dp[-1][-1]
复杂度分析
-
时间复杂度: O ( m n ) O(mn) O(mn)。
-
空间复杂度: O ( m n ) O(mn) O(mn),
63. 不同路径 II
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1 和 0 来表示。
示例 1:
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
- 向右 -> 向右 -> 向下 -> 向下
- 向下 -> 向下 -> 向右 -> 向右
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
'''
和不同路径1的解法,有点出路。
区别就是当碰到障碍物后,其当前值是为0的,因为这个点是不可能到最终点的,并且与该点相关的都不能到达终点
'''
if obstacleGrid[0][0]==1:
return 0
dp=[[0 for i in range(len(obstacleGrid[0]))] for j in range(len(obstacleGrid))] # 所以该矩阵不能全为1的矩阵
dp[0][0]=1
for i in range(len(obstacleGrid)):
for j in range(len(obstacleGrid[0])):
if obstacleGrid[i][j]!=1: # 当不是障碍点,则继续状态转义方程
if i>0 and j>0:
dp[i][j]=dp[i-1][j]+dp[i][j-1]
elif j>0:
dp[i][j]=dp[i][j-1]
elif i>0:
dp[i][j]=dp[i-1][j]
return dp[-1][-1]
复杂度分析
-
时间复杂度: O ( m n ) O(mn) O(mn)。
-
空间复杂度: O ( m n ) O(mn) O(mn),
64. 最小路径和
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
'''
dp问题,要获取数据总和为最小,从最后一个数字出发,即若路径总和为最小,即f[10][10]=min(f[10][9]+grid[10][10],f[9][10]+grid[10][10])
边界:f[0][0]=grid[0][0]
最优子结构:f[10][10] 的最优子结构为f[10][9]和f[9][10]
状态转移:f[10][10]=min(f[10][9]+grid[10][10],f[9][10]+grid[10][10])
'''
row,col=len(grid),len(grid[0])
dp=[[0 for i in range(col)] for j in range(row)]
for i in range(row):
for j in range(col):
if i ==0 and j==0:
dp[i][j]=grid[0][0]
elif i>0 and j==0: # 特殊点
dp[i][j]=dp[i-1][j]+grid[i][j]
elif j>0 and i==0: # 特殊点
dp[i][j]=dp[i][j-1]+grid[i][j]
else:
dp[i][j]=min(dp[i-1][j]+grid[i][j],dp[i][j-1]+grid[i][j])
return dp[-1][-1]
复杂度分析
-
时间复杂度: O ( m n ) O(mn) O(mn)。
-
空间复杂度: O ( m n ) O(mn) O(mn),
1143. 最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3 。
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
# 思路:动态规划,最长公共子序列问题是典型的二维动态规划问题。 定义二维数组,每一个数字存储当前最长的公共序列长度, 时空复杂度 o(nm)
'''
初始条件,首先,对于dp的初始化为0,且长度为text1+1(原因在于,也可以不用+1,但要事先初始化dp[0][i],dp[i][0] 的初始值,也就是要事先遍历两边)
最优子结构,即,有两种情况,相等和不相等,相等,那就是和上一个相等的相关,不相等,但是有两种情况,1.text1的上一个字符的最长公共序列
状态转移:相等,dp[i][j]=dp[i-1][j-1]+1
不相等:dp[i][j]=max(dp[i-1][j],dp[i][j-1])
'''
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]
413. 等差数列划分
如果一个数列 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该数列为等差数列。
例如,[1,3,5,7,9]、[7,7,7,7] 和 [3,-1,-5,-9] 都是等差数列。
给你一个整数数组 nums ,返回数组 nums 中所有为等差数组的 子数组 个数。
子数组 是数组中的一个连续序列。
示例 1:
输入:nums = [1,2,3,4]
输出:3
解释:nums 中有三个子等差数组:[1, 2, 3]、[2, 3, 4] 和 [1,2,3,4] 自身。
class Solution:
def numberOfArithmeticSlices(self, nums: List[int]) -> int:
# 思路1:滑动窗口 [1,2,3,4] 可以这里理解,1234,可以想成123+4,首先123 肯定为1,234也为1,本身也为1,那么表明,当前长度为4,的时候,只有多加了一个本身,
if len(nums)<3:
return 0
ans=0
diff,t=nums[1]-nums[0],0
for i in range(2,len(nums)):
if nums[i]-nums[i-1]==diff:
t+=1
else:
diff=nums[i]-nums[i-1]
t=0
ans+=t # 这里为什么直接加t,因为当连续三个
return ans
120. 三角形最小路径和
给定一个三角形 triangle ,找出自顶向下的最小路径和。
每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。
示例 1:
输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
2
3 4
6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
class Solution:
def minimumTotal(self, triangle: List[List[int]]) -> int:
'''
动态规划:最后一个点的最小路径和,肯定和其上一个点的最小路径和有关。由于每个行都有其n的变化,所以:
f[i][j]=min(f[i-1][j],f[i-1][j-1])+c[i][j]
即三要素:
边界:f[0][0]=c[0][0] 每一行的最左边和最右边,的路径只有一个
最优子结构:f[i][j] 的最优子结构为 f[i-1][j],f[i-1][j-1]
状态转移方程:f[i][j]=min(f[i-1][j],f[i-1][j-1])+c[i][j]
'''
# 行数
f=[[0]*i for i in range(1,len(triangle)+1)]
f[0][0]=triangle[0][0]
for i in range(1,len(triangle)):
f[i][0]=f[i-1][0]+triangle[i][0] # 最左边的路径只有一个
for j in range(1,len(triangle[i-1])):
f[i][j]=min(f[i-1][j-1],f[i-1][j])+triangle[i][j]
f[i][i]=f[i-1][i-1]+triangle[i][i] # 最右边的路径只有一个
return min(f[-1])
复杂度分析
-
时间复杂度: O ( n 2 ) O(n^2) O(n2)。
-
空间复杂度: O ( n 2 ) O(n^2) O(n2),
931. 下降路径最小和
给你一个 n x n 的 方形 整数数组 matrix ,请你找出并返回通过 matrix 的下降路径 的 最小和 。
下降路径 可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列(即位于正下方或者沿对角线向左或者向右的第一个元素)。具体来说,位置 (row, col) 的下一个元素应当是 (row + 1, col - 1)、(row + 1, col) 或者 (row + 1, col + 1) 。
示例 1:
输入:matrix = [[2,1,3],[6,5,4],[7,8,9]]
输出:13
解释:下面是两条和最小的下降路径,用加粗+斜体标注:
[[2,1,3], [[2,1,3],
[6,5,4], [6,5,4],
[7,8,9]] [7,8,9]]
class Solution:
def minFallingPathSum(self, matrix: List[List[int]]) -> int:
'''
由题所示:创建于matrix相同行和列的的矩阵,从数据的规律来说,满足子问题最优解的重叠问题,所以采用动态规划来解决
每一次当前点的最小值为:f[i][j]=min(f[i-1][j] ,f[-1][j-1],f[i-1][j+1])+matrix[i][j]
当为边界时,即最左边 最右边 点比较特征,无左点 或者无右点
即:
边界条件:f[0][i]=matrix[0][i]
最优子结构: f[i][j] 的最优子结构为f[i-1][j],f[i-1][j-1],f[i-1][j+1]
转移矩阵:f[i][j]=min(f[i-1][j] ,f[-1][j-1],f[i-1][j+1])+matrix[i][j]
'''
row,col=len(matrix),len(matrix[0])
matrix_return=matrix
for i in range(1,row):
matrix_return[i][0]=min(matrix_return[i-1][0],matrix_return[i-1][1])+matrix[i][0]
for j in range(1,col-1):
matrix_return[i][j]=min(matrix_return[i-1][j],matrix_return[i-1][j+1],matrix_return[i-1][j-1])+matrix[i][j]
matrix_return[i][-1]=min(matrix_return[i-1][-1],matrix_return[i-1][-2])+matrix[i][-1]
return min(matrix_return[-1])
复杂度分析
-
时间复杂度: O ( n 2 ) O(n^2) O(n2)。
-
空间复杂度: O ( n 2 ) O(n^2) O(n2),
class Solution:
def minFallingPathSum(self, matrix: List[List[int]]) -> int:
# 直接原地修改,则空间复杂度为0(1)
row,col=len(matrix),len(matrix[0])
for i in range(1,row):
matrix[i][0]=min(matrix[i-1][0],matrix[i-1][1])+matrix[i][0]
for j in range(1,col-1):
matrix[i][j]=min(matrix[i-1][j],matrix[i-1][j+1],matrix[i-1][j-1])+matrix[i][j]
matrix[i][-1]=min(matrix[i-1][-1],matrix[i-1][-2])+matrix[i][-1]
return min(matrix[-1])
复杂度分析
-
时间复杂度: O ( n 2 ) O(n^2) O(n2)。
-
空间复杂度: O ( 1 ) O(1) O(1),
300. 最长递增子序列
给你一个整数数组 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:
# 动态规划,重叠子问题,
# [10,9,2,5,3,7,101,18] 实际上就是遍历,存储每个元素其当前的最大长度
# 边界 dp[0]=1
# 最优子结构 dp[n] max(d[n],dp[j]+1) j<n 且nums[i]>nums[j]
# 状态转移 dp[i] = max(dp[i], dp[j] + 1)
if not nums:
return 0
dp = []
for i in range(len(nums)):
dp.append(1)
for j in range(i):
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
复杂度分析
-
时间复杂度: O ( n 2 ) O(n^2) O(n2)。
-
空间复杂度: O ( n ) O(n) O(n),存储每个元素的最大值。
396. 旋转函数
给定一个长度为 n 的整数数组 nums 。
假设 arrk 是数组 nums 顺时针旋转 k 个位置后的数组,我们定义 nums 的 旋转函数 F 为:
F(k) = 0 * arrk[0] + 1 * arrk[1] + … + (n - 1) * arrk[n - 1]
返回 F(0), F(1), …, F(n-1)中的最大值 。
生成的测试用例让答案符合 32 位 整数。
示例 1:
输入: nums = [4,3,2,6]
输出: 26
解释:
F(0) = (0 * 4) + (1 * 3) + (2 * 2) + (3 * 6) = 0 + 3 + 4 + 18 = 25
F(1) = (0 * 6) + (1 * 4) + (2 * 3) + (3 * 2) = 0 + 4 + 6 + 6 = 16
F(2) = (0 * 2) + (1 * 6) + (2 * 4) + (3 * 3) = 0 + 6 + 8 + 9 = 23
F(3) = (0 * 3) + (1 * 2) + (2 * 6) + (3 * 4) = 0 + 2 + 12 + 12 = 26
所以 F(0), F(1), F(2), F(3) 中的最大值是 F(3) = 26 。
class Solution:
def maxRotateFunction(self, nums: List[int]) -> int:
# 动态规划 公式推导
# F(0) = 0*A[0]+1*A[1]+2*A[2]+3*A[3]
# F(1) = 0*A[3]+1*A[0]+2*A[1]+3*A[2]
# F(2) = 0*A[2]+1*A[3]+2*A[0]+3*A[1]
# F(3) = 0*A[1]+1*A[2]+2*A[3]+3*A[0]
# F(1)-F(0) = A[0]+A[1]+A[2]-3*A[3]
# F(2)-F(1) = A[0]+A[1]+A[3]-3*A[2]
# F(3)-F(2) = A[0]+A[2]+A[3]-3*A[1]
# 其中 我们定于sumA = A[0]+A[1]+A[2]+A[3],有意思了
# 比如其中F(3)-F(2) = A[0]+A[2]+A[3]-3*A[1]= sumA - 4*A[1]
# 延伸一下如果是一个长度为n+1的数组,
# F(n) = 0*A[0]+1*A[1]+...+(n)*A[n];
# F(n-1) = 0*A[1]+1*A[2]+2*A[3]+...+(n-1)*A[n]+n*A[0];
# F(n)-F(n-1)= sumA-nA[0] => F(i+1)-F(i)=sumA-n*A[n-i-1]
# 是否如此呢?假设一个长度为n的数组
# F(k) = 0*A[n-k]+...+(k-1)*A[n-1]+k*A[0]+(k+1)*A[1]+...+(n-1)*A[n-k-1]
# F(k+1) = 0*A[n-k-1]+...+(k)*A[n-1] +(k+1)*A[0]+(k+2)*A[1]+...+(n-1)*A[n-1-2]
# F(k+1)-F(k) = sumA - nA[n-k-1]
# 证明结论成立。
# 时间空间都为o(n)
sumA=0
pre_f=0
n=len(nums)
for i in range(n):
sumA+=nums[i]
pre_f+=(nums[i]*i)
# 由公式推导,可以得出递推式 F(k+1)-F(k) = sumA - nA[n-k-1]
max_num=pre_f
for i in range(1,n):
pre_f=sumA-n*nums[n-i]+pre_f
max_num=max(max_num,pre_f)
return max_num
6137. 检查数组是否存在有效划分
给你一个下标从 0 开始的整数数组 nums ,你必须将数组划分为一个或多个 连续 子数组。
如果获得的这些子数组中每个都能满足下述条件 之一 ,则可以称其为数组的一种 有效 划分:
子数组 恰 由 2 个相等元素组成,例如,子数组 [2,2] 。
子数组 恰 由 3 个相等元素组成,例如,子数组 [4,4,4] 。
子数组 恰 由 3 个连续递增元素组成,并且相邻元素之间的差值为 1 。例如,子数组 [3,4,5] ,但是子数组 [1,3,5] 不符合要求。
如果数组 至少 存在一种有效划分,返回 true ,否则,返回 false 。
示例 1:
输入:nums = [4,4,4,5,6]
输出:true
解释:数组可以划分成子数组 [4,4] 和 [4,5,6] 。
这是一种有效划分,所以返回 true 。
class Solution:
def validPartition(self, nums: List[int]) -> bool:
# 线性dp 一般而言 10^5 并且 前后存在关系,使用dp
# 比如
dp=[True]+[False]*len(nums)
for i in range(len(nums)):
if i>0 and dp[i-1] and nums[i]==nums[i-1]: # 这里的话只要求 i-1 是一个False 就行,因为 相同元素,连续至少2个
dp[i+1]=True
if i>1 and dp[i-2] and ((nums[i]==nums[i-1] and nums[i-1]==nums[i-2]) or (nums[i]-nums[i-1] ==1 and nums[i-1]-nums[i-2]==1)): # 使用dp[i-2] 因为 连续的话只能三个为一组,因而 当dp[i-2] 只能为False dp[i+1] 才能为True 因为刚好为3
dp[i+1]=True
print(dp)
return dp[-1]
3.难
1289. 下降路径最小和 II
给你一个整数方阵 arr ,定义「非零偏移下降路径」为:从 arr 数组中的每一行选择一个数字,且按顺序选出来的数字中,相邻数字不在原数组的同一列。
请你返回非零偏移下降路径数字和的最小值。
示例 1:
输入:arr = [[1,2,3],[4,5,6],[7,8,9]]
输出:13
解释:
所有非零偏移下降路径包括:
[1,5,9], [1,5,7], [1,6,7], [1,6,8],
[2,4,8], [2,4,9], [2,6,7], [2,6,8],
[3,4,8], [3,4,9], [3,5,7], [3,5,9]
下降路径中数字和最小的是 [1,5,7] ,所以答案是 13 。
class Solution:
def minFallingPathSum(self, grid: List[List[int]]) -> int:
# 思路:和1其实大致是一样的,不过这样是上一层的不同列的最小值,直接原地修改,则空间复杂度为0(1)
matrix=grid
row,col=len(matrix),len(matrix[0])
for i in range(1,row):
for j in range(col):
l=matrix[i-1].copy()
l.remove(l[j])
matrix[i][j]=min(l)+matrix[i][j]
return min(matrix[-1])
复杂度分析
-
时间复杂度: O ( n 3 ) O(n^3) O(n3)。
-
空间复杂度: O ( 1 ) O(1) O(1),
42. 接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 1:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
class Solution:
def trap(self, height: List[int]) -> int:
# 思路1:动态规划,依次存储左边最大值和右边最大值,最后判断左右的最小值与单前高度的比较。
# 初始状态: 左边最大值:height[0] 右边最大值height[n-1]
# 最优子结构:左边最大值:当前最大值,与上一个最大值,与height单前值比较
# 转台转移矩阵:左边最大值: 左边最大值=max(上一个左边最大值,height当前值)
# 时间复杂度o(n) 空间复杂度o(n)
n=len(height)
leftmax=[height[0]]+[0]*(n-1)
for i in range(1,n):
leftmax[i]=max(leftmax[i-1],height[i])
rightmax=[0]*(n-1)+[height[n-1]]
for i in range(n-2,-1,-1):
rightmax[i]=max(rightmax[i+1],height[i])
return sum([min(leftmax[i],rightmax[i])-height[i] for i in range(n)])
三、背包动态规划
背包问题(Knapsack problem) 是一种组合优化的 NP 完全问题。
背包问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。
有 n 种物品,物品 j 的体积为
v
j
v_{j}
vj, 价值为
w
i
w_{i}
wi, 有一个体积限制 V。如何选择物品使得总体积不超过 V,并使得总价值最大。
这是背包问题最基础的描述,再往下细分还可以把背包问题分成几大类,其中比较基础的是 3 种:01 背包,完全背包,多重背包。
1.01 背包问题
01 背包的问题描述:
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是 w e i g h t [ i ] weight[i] weight[i],得到的价值是 v a l u e [ i ] value[i] value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
这是标准的背包问题,以至于很多同学看了这个自然就会想到背包,甚至都不知道暴力的解法应该怎么解了。
这样其实是没有从底向上去思考,而是习惯性想到了背包,那么暴力的解法应该是怎么样的呢?
每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是
o
(
2
n
)
o(2^n)
o(2n),这里的n表示物品数量。
所以暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!
在下面的讲解中,我举一个例子:
背包最大重量为4。
物品为:
问背包能背的物品最大价值是多少?
以下讲解和图示中出现的数字都是以这个例子为例。
二维dp数组01背包
依然动规五部曲分析一波。
- 确定dp数组以及下标的含义
对于背包问题,有一种写法, 是使用二维数组,即 d p [ i ] [ j ] dp[i][j] dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
只看这个二维数组的定义,大家一定会有点懵,看下面这个图:
要时刻记着这个dp数组的含义,下面的一些步骤都围绕这dp数组的含义进行的, 如果哪里看懵了,就来回顾一下i代表什么,j又代表什么。
- 确定递推公式
再回顾一下 d p [ i ] [ j ] dp[i][j] dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
那么可以有两个方向推出来 d p [ i ] [ j ] dp[i][j] dp[i][j],
- 不放物品i:由 d p [ i − 1 ] [ j ] dp[i - 1][j] dp[i−1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时 d p [ i ] [ j ] dp[i][j] dp[i][j]就是 d p [ i − 1 ] [ j ] dp[i - 1][j] dp[i−1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以被背包内的价值依然和前面相同。)
- 放物品i:由
d
p
[
i
−
1
]
[
j
−
w
e
i
g
h
t
[
i
]
]
dp[i - 1][j - weight[i]]
dp[i−1][j−weight[i]]推出,
d
p
[
i
−
1
]
[
j
−
w
e
i
g
h
t
[
i
]
]
dp[i - 1][j - weight[i]]
dp[i−1][j−weight[i]]为背包容量为
j
−
w
e
i
g
h
t
[
i
]
j - weight[i]
j−weight[i]的时候不放物品i的最大价值,那么
d
p
[
i
−
1
]
[
j
−
w
e
i
g
h
t
[
i
]
]
+
v
a
l
u
e
[
i
]
dp[i - 1][j - weight[i]] + value[i]
dp[i−1][j−weight[i]]+value[i] (物品i的价值),就是背包放物品i得到的最大价值
所以递归公式: d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]) dp[i][j]=max(dp[i−1][j],dp[i−1][j−weight[i]]+value[i]);
- dp数组如何初始化
关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。
首先从 d p [ i ] [ j ] dp[i][j] dp[i][j]的定义出发,如果背包容量j为0的话,即 d p [ i ] [ 0 ] dp[i][0] dp[i][0],无论是选取哪些物品,背包价值总和一定为0。如图:
在看其他情况。
状态转移方程 d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]) dp[i][j]=max(dp[i−1][j],dp[i−1][j−weight[i]]+value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。
d p [ 0 ] [ j ] dp[0][j] dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。
那么很明显当 j < w e i g h t [ 0 ] j < weight[0] j<weight[0]的时候, d p [ 0 ] [ j ] dp[0][j] dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。
当 j > = w e i g h t [ 0 ] j >= weight[0] j>=weight[0]时, d p [ 0 ] [ j ] dp[0][j] dp[0][j] 应该是 v a l u e [ 0 ] value[0] value[0],因为背包容量放足够放编号0物品。
代码初始化如下:
实例:
例题:
416. 分割等和子集
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
、
class Solution:
def canPartition(self, nums: List[int]) -> bool:
# 0-1 背包问题 2维
# sum_nums=sum(nums)
# if sum_nums%2:
# return False
# n=sum_nums//2
# dp=[[0]*(n+1) for i in range(len(nums))] # 初始化2维矩阵,n+1是包括n这个数(python是左包右闭)
# for i in range(n): # 初始化第一行
# dp[0][i]=nums[0]
# for i in range(len(nums)): # 对每一个二元素进行选取
# num=nums[i]
# for j in range(n+1): # 选取每一个元素,判断是否能进行放入到哪个容量中
# if j<num: # j<num 则表明当前容量小于当前元素的值,则不进行放入
# dp[i][j]=dp[i-1][j]
# else: # j>=num 则表明当前容量是大于当前元素的值,可以放入。但是放入后也要与上一个元素进行比较、。
# dp[i][j]=max(dp[i-1][j-num]+num,dp[i-1][j])
# return dp[-1][-1]==n # 最终判断最后一个元素是否和 n相等,
# 状态压缩 将2维压缩到1维,主要是根据规律,可以由 0-1 背包问题的递推式发现,j-w[i],表明,当前元素的值与前面的元素值是存在相关性的,因而我们如果进行压缩的话,是需要从后往前进行遍历(要利用前面的 原始值)。而不是前往后遍历(会修改前面的值)
sum_nums=sum(nums)
if sum_nums%2:
return False
n=sum_nums//2
dp=[0]*(n+1)
for i in range(n+1):
if i>=nums[0]:
dp[i]=nums[0]
for i in range(1,len(nums)):
for j in range(n,nums[i]-1,-1):
dp[j]=max(dp[j],dp[j-nums[i]]+nums[i])
return dp[-1]==n