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 c3、打家劫舍
①数组存储
维护一个状态数组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 p4、完全平方数
-  定义状态: 定义 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_res9、分割等和子集
整体思路如下:
- 当列表中元素个数小于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]








![【c++进阶[五]】list相关接口介绍及list和vector的对比](https://i-blog.csdnimg.cn/direct/d4cc160f5ae84eae8c62c1f9363e3fd2.png)









