代码随想录算法训练营第十八天 | 动态规划系列1,2,3,4

news2024/12/23 3:46:39

动态规划系列1,2,3,4

  • 动态规划理论基础
    • 重点
  • 509 斐波那契数
    • 未看解答自己编写的青春版
    • 重点
    • 代码随想录的代码
    • 我的代码(当天晚上理解后自己编写)
  • 70 爬楼梯
    • 未看解答自己编写的青春版
    • 思考后自己写的代码
    • 重点
    • 代码随想录的代码
    • 我的代码(当天晚上理解后自己编写)
  • 746 使用最小花费爬楼梯
    • 未看解答自己编写的青春版
    • 重点
    • 代码随想录的代码
    • 我的代码(当天晚上理解后自己编写)
  • 动态规划系列1总结
  • 62 不同路径
    • 未看解答自己编写的青春版
    • 重点
    • 代码随想录的代码
    • 我的代码(当天晚上理解后自己编写)
  • 目前为止做了几道题,深刻体悟到了理解dp数组含义的重要性
  • 63 不同路径II
    • 未看解答自己编写的青春版
    • 重点
    • 代码随想录的代码
    • 我的代码(当天晚上理解后自己编写)
  • 做了两道不同路径的题,对代码随想录中提到的一维数组有一点感悟了
  • 343 整数拆分
    • 未看解答自己编写的青春版
    • 重点
    • 代码随想录的代码
    • 我的代码(当天晚上理解后自己编写)
  • 96 不同的二叉搜索树
    • 未看解答自己编写的青春版
    • 重点
    • 代码随想录的代码
    • 我的代码(当天晚上理解后自己编写)
  • 动态规划系列2总结
  • 背包问题理论基础
    • 重点
    • 一维dp数组
    • 二维数组
    • 一维数组
  • 代码随想录推荐:01背包问题,统一使用一维数据编写风格。
  • 419 分割等和子集
    • 未看解答自己编写的青春版
    • 本题自己编写感悟
    • 重点
    • 代码随想录的代码
    • 我的代码(当天晚上理解后自己编写)
  • 698 划分为k个相等的子集
    • 未看解答自己编写的青春版
    • 重点
    • 代码随想录的代码
    • 我的代码(当天晚上理解后自己编写)
  • 473 火柴拼正方形
    • 未看解答自己编写的青春版
    • 重点
    • 代码随想录的代码
    • 我的代码(当天晚上理解后自己编写)
  • 1049 最后一块石头的重量II
    • 未看解答自己编写的青春版
    • 重点
    • 基于419改改就好
    • 代码随想录的代码
    • 我的代码(当天晚上理解后自己编写)
  • 动态规划系列3总结
  • 494 目标和
    • 未看解答自己编写的青春版
    • 重点
    • 这道题目的初始化,很有指导意义
    • 代码随想录的代码
    • 我的代码(当天晚上理解后自己编写)
  • 474 一和零
    • 未看解答自己编写的青春版
    • 重点
    • 代码随想录的代码
    • 我的代码(当天晚上理解后自己编写)
  • 动态规划:完全背包理论基础
    • 重点
    • 代码随想录的代码
  • 518 零钱兑换 II
    • 未看解答自己编写的青春版
    • 重点
    • 代码随想录的代码
    • 我的代码(当天晚上理解后自己编写)
  • 动态规划系列4总结
  • 本博客的“我的代码”部分,在继续完成动态规划的下一部分后,进行编写。

动态规划理论基础

动归题型分类:基础题目,背包问题,打家劫舍,股票问题,子序列问题。

误区:做动态规划的题,不能只关注递推公式。

重点

解决动态规划本质的解题步骤:

1、我们在求解动态规划问题时,一般都会定义一个dp数组,我们要明确地知道dp数组的含义以及下标的含义。

2、递推公式。

3、dp数组如何初始化。

4、遍历顺序。是先遍历背包,还是先遍历物品?为什么有的题,遍历顺序要固定,有的题不管先遍历什么都能通过?采用前序遍历还是后序遍历?

5、打印dp数组。动态规划问题,代码一般比较简单,仅靠代码是很难看出问题的,这时候打印dp数组就显得尤为重要。

上述为动态规划五部曲,可以贯穿整个动态规划系列。

509 斐波那契数

未看解答自己编写的青春版

没看出来哪里用了动态规划

class Solution:
    def fib(self, n: int) -> int:
        
        dp = [i for i in range(n+1)]
        for i in range(n+1):
            if i > 1:
                dp[i] =  dp[i-1] +  dp[i-2]

        return dp[n]

重点

代码随想录的代码

class Solution:
    def fib(self, n: int) -> int:
       
        # 排除 Corner Case
        if n == 0:
            return 0
        
        # 创建 dp table 
        dp = [0] * (n + 1)

        # 初始化 dp 数组
        dp[0] = 0
        dp[1] = 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 <= 1:
            return n
        
        dp = [0, 1]
        
        for i in range(2, n + 1):
            total = dp[0] + dp[1]
            dp[0] = dp[1]
            dp[1] = total
        
        return dp[1]
class Solution:
    def fib(self, n: int) -> int:
        if n <= 1:
            return n
        
        prev1, prev2 = 0, 1
        
        for _ in range(2, n + 1):
            curr = prev1 + prev2
            prev1, prev2 = prev2, curr
        
        return prev2

我的代码(当天晚上理解后自己编写)

70 爬楼梯

未看解答自己编写的青春版

想不出来,想不出递推公式。

这道题在递推公式的卡壳,根本原因就是没有弄明白dp[i]的含义。

思考后自己写的代码

脑子没转过去,已经想到了,当前步和前两步是有关系的,也去想了相加,但怎么就是被困住了,竟然开始纠结前两个值谁大的问题!

当前是N,对于N-1来说,只有一种方式,就是走一步,所以N-1到N的选择个数是确定的,就是N-1的选择数量。同理,从N-2走到N,也只有一种,就是走两步。

所以,N=N-1+N-2

class Solution:
    def climbStairs(self, n: int) -> int:
        if n == 1:
            return 1
        if n == 2:
            return 2
        dp = [0]*(n+1)
        dp[1]=1
        dp[2]=2
        for i in range(3,n+1):
            dp[i] = dp[i-1]+dp[i-2]


        return dp[n]

重点

代码随想录在本题提到的易错点我都没有犯,从一开始的思路,我就没想给dp[0]赋值,因为他是没有意义的,直接从dp[1] dp[2]开始计算就好了。

对于一步可以爬M步的情况,也是很好解决的。

代码随想录的代码

动态规划(版本一)

# 空间复杂度为O(n)版本
class Solution:
    def climbStairs(self, n: int) -> int:
        if n <= 1:
            return n
        
        dp = [0] * (n + 1)
        dp[1] = 1
        dp[2] = 2
        
        for i in range(3, n + 1):
            dp[i] = dp[i - 1] + dp[i - 2]
        
        return dp[n]

动态规划(版本二)

# 空间复杂度为O(3)版本
class Solution:
    def climbStairs(self, n: int) -> int:
        if n <= 1:
            return n
        
        dp = [0] * 3
        dp[1] = 1
        dp[2] = 2
        
        for i in range(3, n + 1):
            total = dp[1] + dp[2]
            dp[1] = dp[2]
            dp[2] = total
        
        return dp[2]

动态规划(版本三)

# 空间复杂度为O(1)版本
class Solution:
    def climbStairs(self, n: int) -> int:
        if n <= 1:
            return n
        
        prev1 = 1
        prev2 = 2
        
        for i in range(3, n + 1):
            total = prev1 + prev2
            prev1 = prev2
            prev2 = total
        
        return prev2

我的代码(当天晚上理解后自己编写)

746 使用最小花费爬楼梯

未看解答自己编写的青春版

倒叙遍历,一维数组就够了,不知道想的对不对?

按照五部曲来说,dp数组含义就是从当前点出发到终点的最小值,递推公式是和遍历顺序相关的我觉得,因为要求最小花费,我觉得从前向后遍历,无法知道从当前处到达终点的最小值,因为还没遍历到。

如果没有明确遍历顺序,甚至还会有声明二维数组的想法,想到我们是知道,从终点的附件到达终点的最小花费的,那么就后序遍历,遍历到最开始,得到的就是最小值。

确定了遍历顺序,递推公式自然就有了,做一个min就好。初始化也是显然的。

class Solution:
    def minCostClimbingStairs(self, cost: List[int]) -> int:
        n = len(cost)
        dp = [0]*n
        dp[n-1]=cost[n-1]
        dp[n-2]=cost[n-2]
        for i in range(n-3,-1,-1):
            dp[i]=min(dp[i+1],dp[i+2])+cost[i]
        return min(dp[0],dp[1])


重点

按照代码随想录解答中的定义:到达第i台阶所花费的最少体力为dp[i]。

这样前序遍历就又可以了!初始化,dp[0]和dp[1]初始化为0就可以,因为这都是可以选择的起点。

也是一种思路!

代码随想录的代码

动态规划(版本一)

class Solution:
    def minCostClimbingStairs(self, cost: List[int]) -> int:
        dp = [0] * (len(cost) + 1)
        dp[0] = 0  # 初始值,表示从起点开始不需要花费体力
        dp[1] = 0  # 初始值,表示经过第一步不需要花费体力
        
        for i in range(2, len(cost) + 1):
            # 在第i步,可以选择从前一步(i-1)花费体力到达当前步,或者从前两步(i-2)花费体力到达当前步
            # 选择其中花费体力较小的路径,加上当前步的花费,更新dp数组
            dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])
        
        return dp[len(cost)]  # 返回到达楼顶的最小花费

动态规划(版本二)

class Solution:
    def minCostClimbingStairs(self, cost: List[int]) -> int:
        dp0 = 0  # 初始值,表示从起点开始不需要花费体力
        dp1 = 0  # 初始值,表示经过第一步不需要花费体力
        
        for i in range(2, len(cost) + 1):
            # 在第i步,可以选择从前一步(i-1)花费体力到达当前步,或者从前两步(i-2)花费体力到达当前步
            # 选择其中花费体力较小的路径,加上当前步的花费,得到当前步的最小花费
            dpi = min(dp1 + cost[i - 1], dp0 + cost[i - 2])
            
            dp0 = dp1  # 更新dp0为前一步的值,即上一次循环中的dp1
            dp1 = dpi  # 更新dp1为当前步的最小花费
        
        return dp1  # 返回到达楼顶的最小花费

我的代码(当天晚上理解后自己编写)

动态规划系列1总结

动态规划系列1总结

62 不同路径

未看解答自己编写的青春版

第一次debug未过主要有以下两点:
1、因为题目未说清楚,以为当m=n=1时,结果为0,没想到是1
2、初始化左边界和上边界,一开始写错了,应该全部初始化为,而不是累加。

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        dp = [[0] * n for _ in range(m)]
        dp[0][0]=1
        for i in range(1,m):
            dp[i][0] = 1
        for i in range(1,n):
            dp[0][i] = 1

        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]

重点

此题也要弄明白dp[i]的含义,是从0走到这里,一共有多少种不同的路径,而不是走了多少步!所以是前两个直接相加,不是错误的+1步。

这道题中有很多值得深挖的地方,详情参加代码随想录的解答文章。
不同路径

大致概括有如下几个方面:深搜会超时,那么深搜的时间复杂度是什么?除了动态规划,还有一种基于组合数的思路,这种思路也有要注意的点,就是要防止溢出,在累乘的过程中不断除以分母。还有就是,基于动态规划的方法,有一个可以节省存储空间的一维数组的方法,类似于滚动数组,也可以学习。

代码随想录的代码

动态规划(版本一)

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        # 创建一个二维列表用于存储唯一路径数
        dp = [[0] * n for _ in range(m)]
        
        # 设置第一行和第一列的基本情况
        for i in range(m):
            dp[i][0] = 1
        for j in range(n):
            dp[0][j] = 1
        
        # 计算每个单元格的唯一路径数
        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:
        # 创建一个一维列表用于存储每列的唯一路径数
        dp = [1] * n
        
        # 计算每个单元格的唯一路径数
        for j in range(1, m):
            for i in range(1, n):
                dp[i] += dp[i - 1]
        
        # 返回右下角单元格的唯一路径数
        return dp[n - 1]

数论

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        numerator = 1  # 分子
        denominator = m - 1  # 分母
        count = m - 1  # 计数器,表示剩余需要计算的乘积项个数
        t = m + n - 2  # 初始乘积项
        while count > 0:
            numerator *= t  # 计算乘积项的分子部分
            t -= 1  # 递减乘积项
            while denominator != 0 and numerator % denominator == 0:
                numerator //= denominator  # 约简分子
                denominator -= 1  # 递减分母
            count -= 1  # 计数器减1,继续下一项的计算
        return numerator  # 返回最终的唯一路径数

我的代码(当天晚上理解后自己编写)

目前为止做了几道题,深刻体悟到了理解dp数组含义的重要性

63 不同路径II

未看解答自己编写的青春版

基于上一题稍作修改,感觉不是很难。

Bebug了几次,主要用于修改边界和初始判断条件。

class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:

       

        m = len(obstacleGrid)
        n = len(obstacleGrid[0])
        
        if obstacleGrid[0][0]==1 or obstacleGrid[m-1][n-1]==1:
            return 0
        dp = [[0] * n for _ in range(m)]
        dp[0][0]=1
        for i in range(1,m):
            if obstacleGrid[i][0]==1 :
                break
            dp[i][0] = 1
        for i in range(1,n):
            if obstacleGrid[0][i]==1 :
                break
            dp[0][i] = 1

        for i in range(1,m):
            for j in range(1,n):
                if obstacleGrid[i][j] != 1 :
                    dp[i][j] = dp[i-1][j]+dp[i][j-1]

        return dp[m-1][n-1]

重点

本题的难点在于,如果在本题中使用一维数组,在递推公式上,要谨慎编写。

代码随想录的代码

动态规划(版本一)

class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid):
        m = len(obstacleGrid)
        n = len(obstacleGrid[0])
        if obstacleGrid[m - 1][n - 1] == 1 or obstacleGrid[0][0] == 1:
            return 0
        dp = [[0] * n for _ in range(m)]
        for i in range(m):
            if obstacleGrid[i][0] == 0:  # 遇到障碍物时,直接退出循环,后面默认都是0
                dp[i][0] = 1
            else:
                break
        for j in range(n):
            if obstacleGrid[0][j] == 0:
                dp[0][j] = 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 - 1][j] + dp[i][j - 1]
        return dp[m - 1][n - 1]

动态规划(版本三),一维数组省空间

class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid):
        if obstacleGrid[0][0] == 1:
            return 0
        
        dp = [0] * len(obstacleGrid[0])  # 创建一个一维列表用于存储路径数
        
        # 初始化第一行的路径数
        for j in range(len(dp)):
            if obstacleGrid[0][j] == 1:
                dp[j] = 0
            elif j == 0:
                dp[j] = 1
            else:
                dp[j] = dp[j - 1]

        # 计算其他行的路径数
        for i in range(1, len(obstacleGrid)):
            for j in range(len(dp)):
                if obstacleGrid[i][j] == 1:
                    dp[j] = 0
                elif j != 0:
                    dp[j] = dp[j] + dp[j - 1]
        
        return dp[-1]  # 返回最后一个元素,即终点的路径数

我的代码(当天晚上理解后自己编写)

做了两道不同路径的题,对代码随想录中提到的一维数组有一点感悟了

递推数组是一维的做法,最关键的一点就是数组是在迭代之间传递的,
dp[j] = dp[j] + dp[j - 1] , 第一个dp代表当前值,第二个dp代表当前值的上方,第三个dp代表当前值的左方。

很巧妙!

343 整数拆分

未看解答自己编写的青春版

自己写了个递归的,但是发现数学逻辑出现错误了,两数相近时,乘积最大,前提是只分解为2个数,但这道题显然不满足这个前提,反例:8。最大值是332,而不是4*4。

没思路没思路。

重点

此题的难点在于,dp为一维数组,但是循环却要两层。

递推公式也不好想,将n拆为 i 和 n-i 后,是取 i*(n-i) 和 i*dp[n-i] 和dp[i] 的最大值,dp[n-i] 代表着拆分为多个值的情况,还要和dp[i]比较是因为,i 是根据我们的循环控制的,不同的 i 对应着不同的值,要取一个最大的。

同时,代码随想录中还提到了一种,编写不清晰,初始化讲不通,仅仅是能运气好通过的代码,也要学习。

同样本题还有一个剪枝的小技巧,这个我想到了。

另外,本题贪心的数学依据,代码随想录并未给出。
整数拆分

代码随想录的代码

动态规划(版本一)

class Solution:
         # 假设对正整数 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]
    def integerBreak(self, n):
        dp = [0] * (n + 1)   # 创建一个大小为n+1的数组来存储计算结果
        dp[2] = 1  # 初始化dp[2]为1,因为当n=2时,只有一个切割方式1+1=2,乘积为1
       
        # 从3开始计算,直到n
        for i in range(3, n + 1):
            # 遍历所有可能的切割点
            for j in range(1, i // 2 + 1):

                # 计算切割点j和剩余部分(i-j)的乘积,并与之前的结果进行比较取较大值
                
                dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j))
        
        return dp[n]  # 返回最终的计算结果

贪心

class Solution:
    def integerBreak(self, n):
        if n == 2:  # 当n等于2时,只有一种拆分方式:1+1=2,乘积为1
            return 1
        if n == 3:  # 当n等于3时,只有一种拆分方式:1+1+1=3,乘积为1
            return 2
        if n == 4:  # 当n等于4时,有两种拆分方式:2+2=4和1+1+1+1=4,乘积都为4
            return 4
        result = 1
        while n > 4:
            result *= 3  # 每次乘以3,因为3的乘积比其他数字更大
            n -= 3  # 每次减去3
        result *= n  # 将剩余的n乘以最后的结果
        return result

我的代码(当天晚上理解后自己编写)

96 不同的二叉搜索树

未看解答自己编写的青春版

依旧想不出递推公式。

差一点就推出来了。

重点

本题的关键。

二叉搜索树的子树也都是二叉搜索树。

递推公式的本质,在于选择 [1,n] 中哪个数作为头结点, 假设选择 i 。

那么左边一定都是比 i 小的数,一共有 i-1 个。

右边一定都是比 i 大的数,一共有 n-i 个。

最后,两边的dp相乘,得出选 i 为头结点时,不同的二叉搜索树的数量。

在这里插入图片描述

代码随想录的代码

class Solution:
    def numTrees(self, n: int) -> int:
        dp = [0] * (n + 1)  # 创建一个长度为n+1的数组,初始化为0
        dp[0] = 1  # 当n为0时,只有一种情况,即空树,所以dp[0] = 1
        for i in range(1, n + 1):  # 遍历从1到n的每个数字
            for j in range(1, i + 1):  # 对于每个数字i,计算以i为根节点的二叉搜索树的数量
                dp[i] += dp[j - 1] * dp[i - j]  # 利用动态规划的思想,累加左子树和右子树的组合数量
        return dp[n]  # 返回以1到n为节点的二叉搜索树的总数量


我的代码(当天晚上理解后自己编写)

动态规划系列2总结

本周的题,我主要失败在递推公式找不到上,而且两题均是只差一点就找到了!

动态规划系列2总结

背包问题理论基础

应对大厂面试,01背包和完全背包就够用了,目前在力扣上也找不到多重背包的题目。

注意,下面所涉及的一些方法,都是01背包的。

重点

对于01背包问题,要先了解其暴力解法,每种物品只有两个状态,【取,不取】,那么我们用回溯算法,遍历所有状态即可。

dp数组的含义:dp[i][j] 下标为[0,i]的物品,任取,放进容量为 j 的背包里。

dp[i][j] 的状态,就取决于,目前,我们放不放物品 i ,放是一个状态,不放又是一个状态。

不放物品 i : dp[i][j] = dp[i-1][j]

放物品 i : dp[i][j] = value(i) + dp[i-1][j-weight[i]]

递推公式:dp[i][j] = max( dp[i-1][j] , value(i) + dp[i-1][j-weight[i]] )

确定如何初始化很重要,可以画一下dp二维数组。

遍历顺序也很关键!二维dp数组实现的01背包,两层for循环是可以颠倒的,先遍历物品,或是先遍历背包,都可以。(在这里,dp数组的行是物品,列是背包容量)。因为根据递推公式,当前物品,是由上方数值和左上方数值,推导出来的。
故而,两层for循环的顺序,不影响。

一维dp数组

那么使用一维dp数组来实现01背包呢?注意,这里的遍历顺序是有讲究的。

为什么可以进行降维,用一维滚动数组来实现呢?

注意我们的递推公式,dp[i][j] = max( dp[i-1][j] , value(i) + dp[i-1][j-weight[i]] )

第 i 层 只和 i-1 层有关,我们可以将 i-1 层的数据,拷贝到第 i 层,然后进行滚动计算,这就是为什么使用一维数组。

一维 dp[j] 含义:容量为 j 的背包所能装的最大价值。同样,状态取决于放不放物品 i ,目前,我们放不放物品 i ,放是一个状态,不放又是一个状态。

不放物品 i : dp[j] = dp[j] (因为这里我们是拷贝的,这里其实是上一层的数据)

放物品 i : dp[j] = dp[j-weight(i)]+value(i)

递推公式 : dp[j] = max( dp[j] , dp[j-weight(i)]+value(i) )

dp数组的初始化:dp[0] = 0,dp[i]要初始化为非负数里的最小值,因为后续会取max,如果初始化为一个正数,可能会影响max操作。所以全部初始化为0.

遍历顺序:先遍历物品,在遍历背包,背包要倒序遍历。(此遍历顺序唯一)

为什么要倒序?保证每个物品只被添加一次。如果是正序,对于最小的物品,就会被多次添加,代码随想录给出的例子很形象。因为我们的数据是从上一层拷贝来的,更新时需要用到此数据,而正序遍历会将数据破坏掉,后面的值无法获得准确的上一层的值,所以也要倒序遍历。

倒序遍历的原因是,本质上还是一个对二维数组的遍历,并且右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖。

为什么二维数据可以正序遍历?因为二维数据的每一行都是解耦的,给 i 行赋值,不会影响我们后面去调用 i-1 行的操作。

为什么必须先遍历物品,再遍历背包?颠倒顺序后,先遍历背包,再遍历物品,但因为我们的背包是倒序遍历的,会导致每个背包只放入一个物品。

二维数组

无参数版

def test_2_wei_bag_problem1():
    weight = [1, 3, 4]
    value = [15, 20, 30]
    bagweight = 4

    # 二维数组
    dp = [[0] * (bagweight + 1) for _ in range(len(weight))]

    # 初始化
    for j in range(weight[0], bagweight + 1):
        dp[0][j] = value[0]

    # weight数组的大小就是物品个数
    for i in range(1, len(weight)):  # 遍历物品
        for j in range(bagweight + 1):  # 遍历背包容量
            if j < weight[i]:
                dp[i][j] = dp[i - 1][j]
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])

    print(dp[len(weight) - 1][bagweight])

test_2_wei_bag_problem1()

有参数版

def test_2_wei_bag_problem1(weight, value, bagweight):
    # 二维数组
    dp = [[0] * (bagweight + 1) for _ in range(len(weight))]

    # 初始化
    for j in range(weight[0], bagweight + 1):
        dp[0][j] = value[0]

    # weight数组的大小就是物品个数
    for i in range(1, len(weight)):  # 遍历物品
        for j in range(bagweight + 1):  # 遍历背包容量
            if j < weight[i]:
                dp[i][j] = dp[i - 1][j]
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])

    return dp[len(weight) - 1][bagweight]

if __name__ == "__main__":

    weight = [1, 3, 4]
    value = [15, 20, 30]
    bagweight = 4

    result = test_2_wei_bag_problem1(weight, value, bagweight)
    print(result)

一维数组

无参版

def test_1_wei_bag_problem():
    weight = [1, 3, 4]
    value = [15, 20, 30]
    bagWeight = 4

    # 初始化
    dp = [0] * (bagWeight + 1)
    for i in range(len(weight)):  # 遍历物品
        for j in range(bagWeight, weight[i] - 1, -1):  # 遍历背包容量
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i])

    print(dp[bagWeight])


test_1_wei_bag_problem()

有参版

def test_1_wei_bag_problem(weight, value, bagWeight):
    # 初始化
    dp = [0] * (bagWeight + 1)
    for i in range(len(weight)):  # 遍历物品
        for j in range(bagWeight, weight[i] - 1, -1):  # 遍历背包容量
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i])

    return dp[bagWeight]


if __name__ == "__main__":

    weight = [1, 3, 4]
    value = [15, 20, 30]
    bagweight = 4

    result = test_1_wei_bag_problem(weight, value, bagweight)
    print(result)

代码随想录推荐:01背包问题,统一使用一维数据编写风格。

419 分割等和子集

未看解答自己编写的青春版

一开始的错误思路:

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        n = len(nums)
        if n == 1:
            return False
        sumsum = sum(nums)
        # weight = [1 1 1 1]
        # value = nums
        # 总重 n
        dp = [0]*n
        for i in range(n):
            for j in range(n-1,0,-1):
                dp[j] = dp[j-1]+nums[i]
                if 2*dp[j] == sumsum :
                    return True
        return False

调了半天,终于过了

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        n = len(nums)
        sumsum = sum(nums)
        if sumsum % 2 == 1:
            return False
        
       
        sum_weight = int(sumsum / 2)
        # weight = nums
        # value = nums
        # sum_weight = sum(nums)/2
        dp = [0]*(sum_weight+1)
        for i in range(n):
            for j in range(sum_weight,nums[i]-1,-1):
                dp[j] = max(dp[j-nums[i]]+nums[i],dp[j])
        if dp[-1] == sum_weight :
            return True
        else :
            return False

本题自己编写感悟

之前我一直没真正写过01背包的代码,这次写的时候发现了几个被我忽视的点:

1、dp数组,不管是二维还是一维的,其长度,均为weight+1 !!! 不是 weight 。
这个也好理解,完全按照背包重量的定义来的,0的时候表明背包最大承重为0.

2、倒序遍历的末尾值: 是 weight[i]-1 , 这表明最多只能遍历到 weight[i] ,表明刚好放进物品 i , 剩余重量为 0 ,不要忘记减一

重点

要明确本题中我们要使用的是01背包,因为元素我们只能用一次。

初始化,重要:

如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。

这样才能让dp数组在递推的过程中取得最大的价值,而不是被初始值覆盖了。

本题题目中 只包含正整数的非空数组,所以非0下标的元素初始化为0就可以了。

本题能用01背包的原因就是,本题让每个物品的weight和value都是nums数组的值,当背包装完后,如果:dp[target]=target,就说明背包被装满了,如果不等于,说明没装满。

代码随想录的代码

二维DP版

class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        
        total_sum = sum(nums)

        if total_sum % 2 != 0:
            return False

        target_sum = total_sum // 2
        dp = [[False] * (target_sum + 1) for _ in range(len(nums) + 1)]

        # 初始化第一行(空子集可以得到和为0)
        for i in range(len(nums) + 1):
            dp[i][0] = True

        for i in range(1, len(nums) + 1):
            for j in range(1, target_sum + 1):
                if j < nums[i - 1]:
                    # 当前数字大于目标和时,无法使用该数字
                    dp[i][j] = dp[i - 1][j]
                else:
                    # 当前数字小于等于目标和时,可以选择使用或不使用该数字
                    dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i - 1]]

        return dp[len(nums)][target_sum]

一维DP版

class Solution:
    def canPartition(self, nums: List[int]) -> bool:

        total_sum = sum(nums)

        if total_sum % 2 != 0:
            return False

        target_sum = total_sum // 2
        dp = [False] * (target_sum + 1)
        dp[0] = True

        for num in nums:
            # 从target_sum逆序迭代到num,步长为-1
            for i in range(target_sum, num - 1, -1):
                dp[i] = dp[i] or dp[i - num]
        return dp[target_sum]

我的代码(当天晚上理解后自己编写)

698 划分为k个相等的子集

未看解答自己编写的青春版

直接从上一题改过来的代码错误,这题很难用动态规划,要用回溯。先跳过吧。等复习动态规划的时候,可以看看这题。

重点

代码随想录的代码

我的代码(当天晚上理解后自己编写)

473 火柴拼正方形

同样很难用动态规划,要用回溯。先跳过吧。等复习动态规划的时候,可以看看这题。

未看解答自己编写的青春版

重点

代码随想录的代码

我的代码(当天晚上理解后自己编写)

1049 最后一块石头的重量II

未看解答自己编写的青春版

想不到怎么用01背包。

只想到了求和,除以2,但是怎么用背包还是不懂。

重点

代码随想录给出的思路:

本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了。

一开始想不太懂,但是再想想就明白了!这两堆石头,根本不用管哪边的石头多,因为相撞后,残留部分会回到原集合中。然后继续相撞!!!

基于419改改就好

class Solution:
    def lastStoneWeightII(self, stones: List[int]) -> int:
        n = len(stones)
        sumsum = sum(stones)
       
        sum_weight = sumsum // 2
        # weight = nums
        # value = nums
        # sum_weight = sum(nums)/2
        dp = [0]*(sum_weight+1)
        for i in range(n):
            for j in range(sum_weight,stones[i]-1,-1):
                dp[j] = max(dp[j-stones[i]]+stones[i],dp[j])
        
        return abs(sumsum-2*dp[-1])

代码随想录的代码

二维DP

class Solution:
    def lastStoneWeightII(self, stones: List[int]) -> int:
        total_sum = sum(stones)
        target = total_sum // 2
        
        # 创建二维dp数组,行数为石头的数量加1,列数为target加1
        # dp[i][j]表示前i个石头能否组成总重量为j
        dp = [[False] * (target + 1) for _ in range(len(stones) + 1)]
        
        # 初始化第一列,表示总重量为0时,前i个石头都能组成
        for i in range(len(stones) + 1):
            dp[i][0] = True
        
        for i in range(1, len(stones) + 1):
            for j in range(1, target + 1):
                # 如果当前石头重量大于当前目标重量j,则无法选择该石头
                if stones[i - 1] > j:
                    dp[i][j] = dp[i - 1][j]
                else:
                    # 可选择该石头或不选择该石头
                    dp[i][j] = dp[i - 1][j] or dp[i - 1][j - stones[i - 1]]
        
        # 找到最大的重量i,使得dp[len(stones)][i]为True
        # 返回总重量减去两倍的最接近总重量一半的重量
        for i in range(target, -1, -1):
            if dp[len(stones)][i]:
                return total_sum - 2 * i
        
        return 0


一维DP

class Solution:
    def lastStoneWeightII(self, stones):
        total_sum = sum(stones)
        target = total_sum // 2
        dp = [False] * (target + 1)
        dp[0] = True

        for stone in stones:
            for j in range(target, stone - 1, -1):
                # 判断当前重量是否可以通过选择之前的石头得到或选择当前石头和之前的石头得到
                dp[j] = dp[j] or dp[j - stone]

        for i in range(target, -1, -1):
            if dp[i]:
                # 返回剩余石头的重量,即总重量减去两倍的最接近总重量一半的重量
                return total_sum - 2 * i

        return 0

我的代码(当天晚上理解后自己编写)

动态规划系列3总结

本周就讲了01背包问题,01背包问题全部用一维DP数组求解。
动态规划系列3总结

494 目标和

未看解答自己编写的青春版

不会,没思路。

重点

转换思路,太重要了!前面放加法和放减法,本质上就是将整个集合,分为了左右两个集合,和前面两题思路又类似了!
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

这道题目的初始化,很有指导意义

有时候不要死扣字面意思,要顺应题意。
在这里插入图片描述

代码随想录的代码

回溯版

class Solution:


    def backtracking(self, candidates, target, total, startIndex, path, result):
        if total == target:
            result.append(path[:])  # 将当前路径的副本添加到结果中
        # 如果 sum + candidates[i] > target,则停止遍历
        for i in range(startIndex, len(candidates)):
            if total + candidates[i] > target:
                break
            total += candidates[i]
            path.append(candidates[i])
            self.backtracking(candidates, target, total, i + 1, path, result)
            total -= candidates[i]
            path.pop()

    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        total = sum(nums)
        if target > total:
            return 0  # 此时没有方案
        if (target + total) % 2 != 0:
            return 0  # 此时没有方案,两个整数相加时要注意数值溢出的问题
        bagSize = (target + total) // 2  # 转化为组合总和问题,bagSize就是目标和

        # 以下是回溯法代码
        result = []
        nums.sort()  # 需要对nums进行排序
        self.backtracking(nums, bagSize, 0, 0, [], result)
        return len(result)

二维DP

class Solution:
    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        total_sum = sum(nums)  # 计算nums的总和
        if abs(target) > total_sum:
            return 0  # 此时没有方案
        if (target + total_sum) % 2 == 1:
            return 0  # 此时没有方案
        target_sum = (target + total_sum) // 2  # 目标和

        # 创建二维动态规划数组,行表示选取的元素数量,列表示累加和
        dp = [[0] * (target_sum + 1) for _ in range(len(nums) + 1)]

        # 初始化状态
        dp[0][0] = 1

        # 动态规划过程
        for i in range(1, len(nums) + 1):
            for j in range(target_sum + 1):
                dp[i][j] = dp[i - 1][j]  # 不选取当前元素
                if j >= nums[i - 1]:
                    dp[i][j] += dp[i - 1][j - nums[i - 1]]  # 选取当前元素

        return dp[len(nums)][target_sum]  # 返回达到目标和的方案数

一维DP

class Solution:
    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        total_sum = sum(nums)  # 计算nums的总和
        if abs(target) > total_sum:
            return 0  # 此时没有方案
        if (target + total_sum) % 2 == 1:
            return 0  # 此时没有方案
        target_sum = (target + total_sum) // 2  # 目标和
        dp = [0] * (target_sum + 1)  # 创建动态规划数组,初始化为0
        dp[0] = 1  # 当目标和为0时,只有一种方案,即什么都不选
        for num in nums:
            for j in range(target_sum, num - 1, -1):
                dp[j] += dp[j - num]  # 状态转移方程,累加不同选择方式的数量
        return dp[target_sum]  # 返回达到目标和的方案数

我的代码(当天晚上理解后自己编写)

474 一和零

未看解答自己编写的青春版

不会,没思路

重点

还是对01背包的应用题不太了解,看了解答之后豁然开朗,其实想到了这是一个二维度的背包,但是再下一步就不懂了,对dp数组的定义还是掌握不牢。

在这里插入图片描述

代码随想录的代码

DP(版本一)

class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        dp = [[0] * (n + 1) for _ in range(m + 1)]  # 创建二维动态规划数组,初始化为0
        for s in strs:  # 遍历物品
            zeroNum = s.count('0')  # 统计0的个数
            oneNum = len(s) - zeroNum  # 统计1的个数
            for i in range(m, zeroNum - 1, -1):  # 遍历背包容量且从后向前遍历
                for j in range(n, oneNum - 1, -1):
                    dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1)  # 状态转移方程
        return dp[m][n]


DP(版本二)

class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        dp = [[0] * (n + 1) for _ in range(m + 1)]  # 创建二维动态规划数组,初始化为0
        # 遍历物品
        for s in strs:
            ones = s.count('1')  # 统计字符串中1的个数
            zeros = s.count('0')  # 统计字符串中0的个数
            # 遍历背包容量且从后向前遍历
            for i in range(m, zeros - 1, -1):
                for j in range(n, ones - 1, -1):
                    dp[i][j] = max(dp[i][j], dp[i - zeros][j - ones] + 1)  # 状态转移方程
        return dp[m][n]

我的代码(当天晚上理解后自己编写)

动态规划:完全背包理论基础

重点

纯完全背包问题,两个循环的顺序是可以颠倒的。

不同于01背包的是,采用前序遍历,这样物品就可以使用无数次了!

代码随想录的博客写的很好!
完全背包

代码随想录的代码

先遍历物品,再遍历背包(有参版)

def test_CompletePack(weight, value, bagWeight):
    dp = [0] * (bagWeight + 1)
    for i in range(len(weight)):  # 遍历物品
        for j in range(weight[i], bagWeight + 1):  # 遍历背包容量
            dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
    return dp[bagWeight]

if __name__ == "__main__":
    weight = [1, 3, 4]
    value = [15, 20, 30]
    bagWeight = 4
    result = test_CompletePack(weight, value, bagWeight)
    print(result)

先遍历背包,再遍历物品(有参版)

def test_CompletePack(weight, value, bagWeight):
    dp = [0] * (bagWeight + 1)
    for j in range(bagWeight + 1):  # 遍历背包容量
        for i in range(len(weight)):  # 遍历物品
            if j - weight[i] >= 0:
                dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
    return dp[bagWeight]


if __name__ == "__main__":
    weight = [1, 3, 4]
    value = [15, 20, 30]
    bagWeight = 4
    result = test_CompletePack(weight, value, bagWeight)
    print(result)

518 零钱兑换 II

未看解答自己编写的青春版

就是一个完全背包问题,递推公式是前面接触过的,各个值的加和。遍历顺序就采用先遍历物品,再遍历背包。

重点

看了代码随想录的解答才知道,本题的遍历顺序有考究!必须先遍历物品,再遍历背包,因为这题求得是组合数,不强调元素的顺序。

简单来讲,先遍历物品,再遍历背包,得到的是组合数,只有 【1,2】这种情况,没有【2,1】这种情况。而先遍历背包,再遍历物品,则每个背包下,都有【1,2】【2,1】这两种组合,所以这是求得排列数。

代码随想录的代码

class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        dp = [0]*(amount + 1)
        dp[0] = 1
        # 遍历物品
        for i in range(len(coins)):
            # 遍历背包
            for j in range(coins[i], amount + 1):
                dp[j] += dp[j - coins[i]]
        return dp[amount]

我的代码(当天晚上理解后自己编写)

动态规划系列4总结

动态规划系列4总结

本博客的“我的代码”部分,在继续完成动态规划的下一部分后,进行编写。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/758793.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

职责链模式:如何实现可灵活扩展算法的敏感信息过滤框架?

今天&#xff0c;我们主要讲解职责链模式的原理和实现。除此之外&#xff0c;我还会利用职责链模式&#xff0c;带你实现一个可以灵活扩展算法的敏感词过滤框架。下一节课&#xff0c;我们会更加贴近实战&#xff0c;通过剖析Servlet Filter、Spring Interceptor来看&#xff0…

从零开始的python入门之路

❤️ 专栏简介&#xff1a;本专栏记录了我个人从零开始学习Python编程的过程。在这个专栏中&#xff0c;我将分享我在学习Python的过程中的学习笔记、学习路线以及各个知识点。 ☀️ 专栏适用人群 &#xff1a;本专栏适用于希望学习Python编程的初学者和有一定编程基础的人。无…

c语言播放MP3音乐

今天学习了一下怎么用c语言播放音乐的方法&#xff0c;为了防止忘记&#xff0c;特此记录一下&#xff0c;首先就是把要播放的mp3文件保存到和要编辑的.c文件同一个目录中&#xff0c;如图所示&#xff1a; 下面就直接看代码吧&#xff1a; #include<stdio.h> #include&…

线程中并发安全问题(Sychronized关键字的底层原理)

线程中并发安全问题 Sychronized关键字的底层原理 ​ sychronized对象锁采用互斥方式让同一时刻至多只有一个线程能持有对象锁&#xff0c;其他线程想获取这个对象锁只能被阻塞。 Monitor Sychronized的底层实现Monitor。 WaitSet&#xff1a;关联调用了wait方法的线程&a…

DVDNET A FAST NETWORK FOR DEEP VIDEO DENOISING

DVDNET: A FAST NETWORK FOR DEEP VIDEO DENOISING https://ieeexplore.ieee.org/document/8803136 摘要 现有的最先进视频去噪算法是基于补丁的方法&#xff0c;以往的基于NN的算在其性能上无法与其媲美。但是本文提出NN的视频去噪算法性能要好&#xff1a; 其相比于基于补丁…

【板栗糖GIS】——buzz字幕软件的安装和使用

【板栗糖GIS】——buzz字幕软件的安装和使用 1. 下载buzz软件 链接如下&#xff1a; 下载软件包&#xff0c;我已经准备好资源&#xff0c;只是审核还未通过&#xff0c;过两天会加上&#xff0c;忘记补充链接可以私信我 2. 双击安装 直接下一步下一步 3. 使用方法介绍 运…

并发事务会有哪些问题?

并发事务会有哪些问题&#xff1f; 多个事务并发的执行一定会出现相互争夺资源的问题。那么问题具体有哪些呢&#xff1f; 脏写&#xff08;丢失修改&#xff09;脏读不可重复读幻读 以上这四个问题就是我们需要知道的。但是脏写&#xff0c;由于mysql最低的隔离级别都能避免…

gradle的下载、解压、环境变量配置以及命令行基本用法

父目录 Android 开发入门 - wuyujin1997 文章目录 Intro下载解压配置环境变量环境变量测试 命令行命令行基本用法命令行更多用法 Intro 关于Java项目的依赖、编译流程等管理&#xff0c;有三代的解决方案。 AntMavenGradle 可以想像这三代工具必定是各有特定&#xff0c;但整…

【算法 -- LeetCode】(020) 有效的括号

1、题目 给定一个只包括 ‘(’&#xff0c;‘)’&#xff0c;‘{’&#xff0c;‘}’&#xff0c;‘[’&#xff0c;‘]’ 的字符串 s &#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。每个…

javaagent简单理解

1. javajaent是什么 javaagent可以理解为是一个插件&#xff0c;需要有一个jvm进程才能运行。例如arthas这个工具就是用到了javaagent。 2. 如何使用 1. 一种调用方式&#xff1a;java -javaagent:path[参数参数值] 2. 方法签名 public static void premain(String args, I…

从零玩转系列之SpringBoot3-核心原理

一、简介 1.前置知识 ● Java17 ● Spring、SpringMVC、MyBatis ● Maven、IDEA 2.环境要求 环境&工具版本(or later)SpringBoot3.1.xIDEA2023.xJava17Maven3.5Tomcat10.0Servlet5.0GraalVM Community22.3Native Build Tools0.9.19 二、SpringBoot3-核心原理 1.事件和监听器…

【Hippo4j监控Web容器Tomcat线程池】

&#x1f680; 线程池管理工具-Hippo4j &#x1f680; &#x1f332; AI工具、AI绘图、AI专栏 &#x1f340; &#x1f332; 如果你想学到最前沿、最火爆的技术&#xff0c;赶快加入吧✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;CSDN-Java领域优质创作者&#…

知识整合:Web页面请求的历程

Web页面请求的历程 内部涉及知识&#xff1a;一、准备:DHCP、UDP、IP 和以太网二、仍在准备&#xff1a;DNS和ARP三、仍在准备&#xff1a;域内路由选择到DNS服务器四、Web客户-服务器交互&#xff1a;TCP和HTTP五、HTTP请求响应格式Requests部分Responses 部分 下载一个Web页面…

2023.7.15

同余最短路 P3403 跳楼机 题意&#xff1a;给定h高的楼层&#xff0c;起始位置在第一层&#xff0c;可以选择操作向上移动x层或y层或z层&#xff0c;回到第一层 求可以到达的楼层数 思路&#xff1a;转化题意为求axbyczk(k在[1,h]&#xff0c;x,y,z为正整数,有多少k满足条件&am…

基础IO

1.C的文件接口 "r" - 只读模式&#xff0c;打开文件用于读取&#xff0c;文件必须存在。 "w" - 写模式&#xff0c;打开文件用于写入&#xff0c;如果文件已存在则清空文件内容&#xff0c;如果文件不存在则创建新文件。 "a" - 追加模式&#…

手把手搭建mybatis入门程序

目录 准备数据库表 搭建工程 引入日志框架lockback SqlSessionUtil工具类封装 准备数据库表 CREATE TABLE t_car (id bigint NOT NULL AUTO_INCREMENT COMMENT 主键,car_num varchar(100) DEFAULT NULL COMMENT 汽车编号,brand varchar(100) DEFAULT NULL COMMENT 品牌,gui…

阿里云2核4G服务器能搭建几个网站?性能如何?

2核4G服务器能安装多少个网站&#xff1f;2核4g配置能承载多少个网站&#xff1f;一台2核4G服务器可以安装多少个网站&#xff1f;阿腾云2核4G5M带宽服务器目前安装了14个网站&#xff0c;从技术角度是没有限制的&#xff0c;只要云服务器性能够用&#xff0c;想安装几个网站就…

Java正则表达式校验某个字符串是否是合格的email

Java正则表达式校验某个字符串是否是合格的email 可以借助正则表达式校验某个字符串是否是合规的电子邮箱。对于邮箱的正则表达式有严格的模式&#xff0c;如&#xff1a;^[a-zA-Z0-9_&*-](?:\\.[a-zA-Z0-9_&*-])*(?:[a-zA-Z0-9-]\\.)[a-zA-Z]{2,7}$ 对应的Java实现…

Verilog基础之十六、RAM实现

目录 一、前言 二、工程设计 2.1 RAM IP核使用 2.2 设计代码 2.3 仿真代码 2.4 综合结果 2.5 仿真结果 一、前言 工程设计中除逻辑计算单元外&#xff0c;存储单元也是不可获取的部分&#xff0c;RAM(Random Access Memory)随机存取存储器即可以写入数据&#xff0c;也可…

Spring Cloud Gateway下的GC停顿排查之旅

01 背景 在微服务架构体系流行的当下&#xff0c;Spring Cloud全家桶已经是大多数团队的首选&#xff0c;我们也不例外&#xff0c;并且选择了Spring Cloud Gateway作为了业务网关&#xff0c;进行了一些通用能力的开发&#xff0c;如鉴权、路由等等。作为一个成熟的框架&#…