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 n
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[i]
表示最少需要多少个完全平方数的和来表示整数i
。 -
状态转移方程: 对于每个数
i
,尝试减去一个完全平方数j²(其中j²<=i
),此时状态转移方程为:dp[i]=min(dp[i],dp[i−j²]+1),这里的dp[i-j^2]
表示去掉一个完全平方数j²后,剩下的数的最小完全平方数数量,加上 1 是因为用了一个j²。 -
初始化: 对于
dp[0]
,需要 0 个完全平方数来表示;其余dp[i]
初始化为正无穷,表示暂时未知最小值。 -
结果:
dp[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[i]
表示组成金额i
需要的最少硬币数。 -
状态转移方程: 对于每个金额
dp[i]=min(dp[i],dp[i−coin]+1),i
,尝试减去一种硬币的面额coin
,此时状态转移方程为:dp[i-coin]
表示减去硬币coin
之后剩余金额的最小硬币数,加 1 是因为用了一个coin
。 -
初始化:
dp[0] = 0
,表示凑成金额 0 需要 0 个硬币。- 其余的
dp[i]
初始化为无穷大,表示暂时还无法凑出这些金额。
-
结果: 最终的结果是
dp[amount]
,如果dp[amount]
仍然是无穷大,说明无法凑成该金额,返回 -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)
d=[]
for i in range(n):
if not d or nums[i]>d[-1]:
d.append(nums[i])
else:
l,r=0,len(d)-1
while l<r:
mid=(l+r)//2
if d[mid]>=nums[i]:
r=mid
else:
l=mid+1
d[l]=nums[i]
return len(d)
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]