1、爬楼梯
①动态规划
(1)时间复杂度 O(n) ,空间复杂度 O(n)的做法
开辟一个长度为 n+1 的状态数组f,f[i]表示走到第i个台阶的方案数。初始化f[0]=1(在台阶底部,不需要移动也视为一种方法),f[1]=1(走到台阶1的方案只有一种,就是爬一步)。
爬楼梯的状态转移公式是f[i]=f[i-1]+f[i-2],因为走到第i个台阶,必然是从第i-1个台阶或者第i-2个台阶上爬上来的,因此走到第i个台阶的方案数等于走到第i-1个台阶的方案数与走到第i-2个台阶的方案数之和。
最后返回f(n)就是爬到n级阶梯的方案总数。
class Solution:
def climbStairs(self, n: int) -> int:
if n<=1:
return 1
f=[0]*(n+1)
f[0],f[1]=1,1
for i in range(2,n+1):
f[i]=f[i-1]+f[i-2]
return f[n]
(2)时间复杂度 O(n) ,空间复杂度 O(1)的做法
采用滚动数组思想,将空间复杂度优化到O(1)。

a 和 b 分别存储了到达当前台阶前的两个状态的爬法数量。循环每次迭代时,a 和 b 依次滚动更新,使得 a 总是 b 的前一个状态,而 b 总是当前状态。
class Solution:
def climbStairs(self, n: int) -> int:
if n<=1:
return n
a,b=1,1
for i in range(2,n+1):
a,b=b,a+b
return b
②爬楼梯进阶
题目描述:给定n阶台阶,一次可以跳1到n阶,计算有多少种不同的方法可以从地面跳到第n阶台阶。
(1)时间复杂度 O(n²)的做法
dp[i]=dp[i-1]+dp[i-2]+...+dp[1]+dp[0]
class Solution:
def climbStairs(self, n: int) -> int:
if n<=1:
return 1
dp=[0]*(n+1)
dp[0]=1
for i in range(1,n+1):
for j in range(i):
dp[i]+=dp[j]
return dp[n]
(2)优化后时间复杂度O(n)的做法
dp[i]=dp[i-1]+dp[i-2]+...+dp[1]+dp[0],而dp[i-1]=dp[i-2]+...+dp[1]+dp[0],所以可以得到dp[i]=dp[i-1]*2。
def climbStairs(n: int) -> int:
dp = [1] * (n + 1)
for i in range(2, n + 1):
dp[i] = 2 * dp[i - 1]
return dp[n]
也可以维护一个total_sum变量记录到目前为止的累积和。
def climbStairs(n: int) -> int:
dp=[0]*(n+1);dp[0]=1;dp[1]=1
total_sum=dp[0]+dp[1]
for i in range(2,n+1):
dp[i]=total_sum
total_sum+=dp[i]
return dp[n]
2、杨辉三角

左上角和右上角同时有元素的元素状态转移式:c[i][j]=c[i−1][j−1]+c[i−1][j] 。
class Solution:
def generate(self, numRows: int) -> List[List[int]]:
c=[[1]*(i+1) for i in range(numRows)]
for i in range(2,numRows):
for j in range(1,i):
c[i][j]=c[i-1][j-1]+c[i-1][j]
return c
3、打家劫舍
①数组存储
维护一个状态数组dp,dp[i]表示打劫前i个房子所能获得的最大收益数。
由于打劫了当前房子就不能打劫邻近的房子,因此状态转移方程如下所示:
dp[i]=max(dp[i-1],dp[i-2]+nums[i])
class Solution:
def rob(self, nums: List[int]) -> int:
if not nums:
return 0
n=len(nums)
if n==1:
return nums[0]
dp=[0]*n
dp[0]=nums[0]
dp[1]=max(nums[0],nums[1])
for i in range(2,n):
dp[i]=max(dp[i-2]+nums[i],dp[i-1])
return dp[n-1]
②滚动数组
用dp[i-1]更新r,用dp[i]更新p,滚动下去p=dp[n]就是最后答案。
class Solution:
def rob(self, nums: List[int]) -> int:
if not nums:
return 0
n=len(nums)
if n==1:
return nums[0]
r=nums[0]
p=max(nums[0],nums[1])
for i in range(2,n):
r,p=p,max(p,r+nums[i])
return p
4、完全平方数
-
状态转移数组定义:维护一个数组dp,dp[i]表示和为i的最少平方数数量。
-
状态转移方程:dp[i]=min(dp[i],dp[i−j²]+1),这里的 dp[i-j^2] 表示去掉一个完全平方数j²后,剩下的数的最小完全平方数数量,加上 1 是因为用了一个j²。
-
初始化:因为找到是最少数量,所以初始化dp中元素初值为float('inf'),而根据题意可知,0不参与完全平方数的构建,因此dp[0]=0。
-
遍历数组:外层循环遍历1到n,内层循环遍历1到n的开方。
class Solution:
def numSquares(self, n: int) -> int:
dp=[float('inf')]*(n+1)
dp[0]=0
for i in range(1,n+1):
for j in range(1,int(math.sqrt(i))+1):
dp[i]=min(dp[i],dp[i-j*j]+1)
return dp[n]
5、零钱兑换
- 状态数组:维护一个状态数组dp,dp[i]记录了组成金额i所需的最少金额数。
- 状态转移方程:dp[i]=min(dp[i],dp[i-coin]+1)。
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
if amount==0:
return 0
dp=[float('inf')]*(amount+1)
dp[0]=0
for i in range(1,amount+1):
for coin in coins:
if i-coin>=0:
dp[i]=min(dp[i],dp[i-coin]+1)
return dp[amount] if dp[amount]!=float('inf') else -1
6、单词拆分
首先将wordDict转换成集合,因为在集合中查找的效率更高些。
-
定义状态: 我们定义一个布尔数组
dp,其中dp[i]表示前i个字符的子字符串s[0:i]是否可以由wordDict中的单词拆分。 -
状态转移方程: 对于每个
i,需要检查在s[0:i]之前的每一个分割点j,如果dp[j]为True,且s[j:i]在wordSet 中,那么dp[i]就可以被置为True,表示可以拆分成合法的单词组合:dp[i]=dp[j]∧(s[j:i]∈wordSet)。 -
初始化:
dp[0] = True,表示空字符串可以被成功拆分。 -
结果: 最终
dp[n]就表示整个字符串是否可以被成功拆分。
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
n=len(s)
wordSet=set(wordDict)
dp=[False]*(n+1)
dp[0]=True
for i in range(1,n+1):
for j in range(i):
if dp[j] and s[j:i] in wordSet:
dp[i]=True
break
return dp[n]
7、最长递增子序列
①动态规划
-
定义状态: 我们使用一个数组
dp,其中dp[i]表示以第i个元素结尾的最长递增子序列的长度。 -
状态转移方程: 对于每个元素
nums[i],我们遍历它之前的所有元素nums[j](j < i),如果nums[i] > nums[j],则表示nums[i]可以接在nums[j]后面构成递增子序列,因此:dp[i]=max(dp[i],dp[j]+1)。其中dp[j]是以nums[j]结尾的最长递增子序列的长度,加上 1 表示再加上当前元素nums[i]。 -
初始化: 每个元素都至少可以作为一个长度为 1 的子序列,因此
dp数组初始化为全 1。 -
结果: 最终答案是
dp数组中的最大值,即最长递增子序列的长度。
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
if not nums:
return 0
n=len(nums)
dp=[1]*n
for i in range(1,n):
for j in range(i):
if nums[i]>nums[j]:
dp[i]=max(dp[i],dp[j]+1)
return max(dp)
②贪心+二分查找
维护一个序列d来存储当前得到的最大递增子序列。
让序列 d 尽可能保持递增,并且在可以替换的情况下,优先用较小的值来替换 d 中的某个元素,这样就有更多机会在未来找到更长的递增子序列。
具体如下:
每次遍历数组时,考虑当前数字 nums[i]:
- 如果
nums[i]比序列d中最后一个元素d[-1]还大,就把nums[i]加入d,增长序列d。 - 否则,我们在序列
d中找到第一个大于等于n的元素,用n替换它。这个操作是为了尽可能地保持较小的值,从而增加后续的递增潜力。
比如说,nums=[1,4,2,3,5]。初始d=[],d=[1],d=[1,4],d=[1,2],d=[1,2,3],d=[1,2,3,5]。
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
if not nums:
return 0
n=len(nums)
ans=[nums[0]]
def bin_search(num,ans):
l,r=0,len(ans)-1
while l<r:
mid=(l+r)//2
if num<=ans[mid]:
r=mid
else:
l=mid+1
return l
for i in range(1,n):
if nums[i]>ans[-1]:
ans.append(nums[i])
else:
pos=bin_search(nums[i],ans)
ans[pos]=nums[i]
return len(ans)
8、乘积最大子数组
由于负数乘积可能使得结果反转为正数,因此在处理乘积问题时,除了维护当前的最大值,还需要同时维护当前的最小值(因为负数乘以负数可能会变成正数)。
维护三个变量max_product、min_product、max_global,分别记录当前以 i 结尾的子数组的最大乘积、当前以 i 结尾的子数组的最小乘积和全局最大乘积。
遍历数组中每个元素:
- 当前元素为负数:max_product和min_product交换一下,因为负数乘上负数是正值。
- 当前元素不为负:比较、更新max_production、min_production和max_product。
class Solution:
def maxProduct(self, nums: List[int]) -> int:
max_res=max_product=min_product=nums[0]
n=len(nums)
for i in range(1,n):
if nums[i]<0:
max_product,min_product=min_product,max_product
max_product=max(nums[i],nums[i]*max_product)
min_product=min(nums[i],nums[i]*min_product)
max_res=max(max_res,max_product)
return max_res
9、分割等和子集
整体思路如下:
- 当列表中元素个数小于2,分割不了等和子集,直接返回
False。 - 首先,如果数组的总和是奇数,那么肯定无法将其分成两个和相等的子集,直接返回
False。 - 如果总和是偶数,目标就是找出是否可以从数组中挑选出一些数字,它们的和等于数组总和的一半(即
sum(nums)// 2)。
这样问题就转换成了一个0-1背包问题,可以把这个问题看作一个容量为half_sum的背包,数组中的每个数字就是物品,问是否能够恰好填满这个背包。
- 使用一个布尔数组
dp,其中dp[i]表示是否存在子集和等于i。 - 状态转移:对于每个数字
num,我们更新dp数组的状态。如果dp[j-num]是True,则dp[j]也应为True,即表示我们可以通过加入当前的num形成和为j的子集。【dp[j]=dp[j] or dp[j−num]】 - 最终,检查
dp[half_num]是否为True,如果是,则说明可以找到一个子集和等于目标值。
class Solution:
def canPartition(self, nums: List[int]) -> bool:
if len(nums)<2:
return False
total_sum=sum(nums)
if total_sum%2==1:
return False
half_sum=total_sum//2
dp=[False]*(half_sum+1)
dp[0]=True
for num in nums:
for j in range(half_sum,num-1,-1):
dp[j]=dp[j] or dp[j-num]
return dp[half_sum]
10、最长有效括号
主要思路如下:
状态数组dp:开辟一个长度为n的状态数组dp,dp[i] 表示 以位置 i 结尾的最长有效括号子串的长度。
状态转移:只有遇到 ) 的时候才能开始判断括号闭合得到有效括号,遇到 ) 分两种情况进行处理,已知当前遍历到了第i个元素,s[i]=')'。
- s[i-1]='(':此时两个括号可以形成有效闭合。dp[i]=dp[i-2]+2(当i-2>=0的时候),s[i-2]表示前i-2位最长有效括号的长度,而加上的2正是当前处理的有效闭合括号个数()【也就是s[i-1]和s[i]两个括号)。
- s[i-1]=')':此时两个括号不能形成有效闭合,因此需要找到当前 i 指向的右括号所匹配的左括号位置。已知dp[i-1]是前 i-1 位中最长有效括号的长度,因此可知,i-dp[i-1]-1是距离i最近的可能是未匹配左括号的位置。若s[i-dp[i-1]-1]='(',则又找到了一个括号匹配对,因此dp[i]=dp[i-1]+dp[i-dp[i-1]-2]+2(当i-dp[i-1]-2>=0时)。【dp[i-dp[i-1]-2]表示在匹配的左括号之前,是否还有其他有效的括号子串】。比如(()(()()())在以上括号子串中,dp[i-dp[i-1]-2]=2(紫色部分),dp[i-1]=6(红色部分),2(绿色部分)。
class Solution:
def longestValidParentheses(self, s: str) -> int:
n=len(s)
if n==0:
return 0
dp=[0]*n
res=0
for i in range(1,n):
if s[i]==')':
if s[i-1]=='(':
dp[i]=dp[i-2]+2 if i-2>=0 else 2
elif i-dp[i-1]-1>=0 and s[i-dp[i-1]-1]=='(':
dp[i]=dp[i-1]+dp[i-dp[i-1]-2]+2 if i-dp[i-1]-2>=0 else dp[i-1]+2
res=max(res,dp[i])
return res
动态规划&spm=1001.2101.3001.5002&articleId=142004334&d=1&t=3&u=a0e1842f3f63490e888a5cc9f7ea4c81)
1322

被折叠的 条评论
为什么被折叠?



