算法母题30道(第5篇):动态规划(21-25)

系列说明:本系列共6篇文章,涵盖30道经典算法母题,使用Java实现
适用场景:算法学习、面试准备、编程能力提升


30道算法母题总览

第1篇:数组与字符串基础(1-5)

  1. 两数之和
  2. 三数之和
  3. 盛最多水的容器
  4. 接雨水
  5. 最长无重复字符子串

第2篇:链表操作(6-10)

  1. 反转链表
  2. 合并两个有序链表
  3. 链表中倒数第k个节点
  4. 判断链表是否有环
  5. 合并K个有序链表

第3篇:栈与队列(11-15)

  1. 有效的括号
  2. 用栈实现队列
  3. 最小栈
  4. 滑动窗口最大值
  5. 每日温度

第4篇:二叉树遍历(16-20)

  1. 二叉树的前序遍历
  2. 二叉树的中序遍历
  3. 二叉树的后序遍历
  4. 二叉树的层序遍历
  5. 二叉树的最大深度

第5篇:动态规划(21-25)⭐ 本篇

  1. 爬楼梯
  2. 最长递增子序列
  3. 最长公共子序列
  4. 背包问题(0-1背包)
  5. 编辑距离

第6篇:搜索与排序(26-30)

  1. 二分查找
  2. 快速排序
  3. 归并排序
  4. 岛屿数量(DFS/BFS)
  5. 全排列(回溯)

动态规划核心思想

动态规划(Dynamic Programming,DP) 是一种通过将复杂问题分解为简单子问题来求解的方法。

核心特征

  1. 重叠子问题:子问题会被重复计算
  2. 最优子结构:全局最优解包含局部最优解
  3. 状态转移方程:子问题之间的关系

解题步骤

  1. 确定状态(dp 数组的含义)
  2. 确定状态转移方程
  3. 确定初始条件(base case)
  4. 确定计算顺序
  5. 空间优化(可选)

第21题:爬楼梯

📝 题目描述

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例

输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶

💡 解题思路

动态规划

  1. 状态定义dp[i] 表示爬到第 i 阶的方法数
  2. 状态转移方程dp[i] = dp[i-1] + dp[i-2]
    • 到达第 i 阶,可以从第 i-1 阶爬 1 步,或从第 i-2 阶爬 2 步
  3. 初始条件dp[0] = 1, dp[1] = 1
  4. 计算顺序:从底向上(0 → n)

本质:这就是斐波那契数列

🎯 日常应用

  • 路径规划:计算不同路径的数量
  • 组合优化:资源分配的方案数
  • 游戏设计:角色移动的可能性
  • 金融模型:多步决策问题

🔧 代码解决的问题

  • 计数问题
  • 递推关系的应用
  • 空间优化的技巧

💻 Java实现代码

public class ClimbingStairs {
    
    /**
     * 爬楼梯 - 动态规划
     * @param n 楼梯阶数
     * @return 不同的方法数
     */
    public int climbStairs(int n) {
        if (n <= 2) {
            return n;
        }
        
        // dp 数组
        int[] dp = new int[n + 1];
        dp[1] = 1;
        dp[2] = 2;
        
        // 状态转移
        for (int i = 3; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        
        return dp[n];
    }
    
    /**
     * 爬楼梯 - 空间优化(只保留前两个状态)
     */
    public int climbStairsOptimized(int n) {
        if (n <= 2) {
            return n;
        }
        
        int prev2 = 1; // dp[i-2]
        int prev1 = 2; // dp[i-1]
        int current = 0;
        
        for (int i = 3; i <= n; i++) {
            current = prev1 + prev2;
            prev2 = prev1;
            prev1 = current;
        }
        
        return current;
    }
    
    // 测试方法
    public static void main(String[] args) {
        ClimbingStairs solution = new ClimbingStairs();
        
        for (int n = 1; n <= 10; n++) {
            System.out.println("n = " + n + ": " + 
                solution.climbStairs(n) + " 种方法");
        }
    }
}

️ 复杂度分析

基础 DP

  • 时间复杂度:O(n) - 一次遍历
  • 空间复杂度:O(n) - dp 数组

空间优化版

  • 时间复杂度:O(n)
  • 空间复杂度:O(1) - 只使用常数个变量

第22题:最长递增子序列

📝 题目描述

给你一个整数数组 nums,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。

示例

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4。

输入:nums = [0,1,0,3,2,3]
输出:4

输入:nums = [7,7,7,7,7,7,7]
输出:1

💡 解题思路

方法1:动态规划 O(n²)

  1. 状态定义dp[i] 表示以 nums[i] 结尾的最长递增子序列长度
  2. 状态转移方程
    dp[i] = max(dp[j]) + 1, 其中 0 ≤ j < i 且 nums[j] < nums[i]
    
  3. 初始条件dp[i] = 1(每个元素本身就是一个递增子序列)

方法2:贪心 + 二分查找 O(n log n)

  1. 维护一个数组 tails,其中 tails[i] 表示长度为 i+1 的递增子序列的最小尾部元素
  2. 遍历数组,对每个元素:
    • 如果大于 tails 的所有元素,直接追加
    • 否则,用二分查找找到第一个 ≥ 当前元素的位置并替换

日常应用

  • 股票交易:最长上涨趋势
  • 数据分析:趋势识别
  • 生物信息学:基因序列比对
  • 性能监控:指标增长趋势

🔧 代码解决的问题

  • 子序列问题
  • 最优子结构
  • 贪心策略的应用

Java实现代码

import java.util.Arrays;

public class LongestIncreasingSubsequence {
    
    /**
     * 最长递增子序列 - 动态规划 O(n²)
     * @param nums 输入数组
     * @return 最长递增子序列的长度
     */
    public int lengthOfLIS(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        int n = nums.length;
        int[] dp = new int[n];
        Arrays.fill(dp, 1);
        
        int maxLength = 1;
        
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            maxLength = Math.max(maxLength, dp[i]);
        }
        
        return maxLength;
    }
    
    /**
     * 最长递增子序列 - 贪心 + 二分查找 O(n log n)
     */
    public int lengthOfLISOptimized(int[] nums) {
        if (nums == null || nums.length == 0) {
            return 0;
        }
        
        // tails[i] 表示长度为 i+1 的递增子序列的最小尾部元素
        int[] tails = new int[nums.length];
        int size = 0;
        
        for (int num : nums) {
            // 二分查找第一个 >= num 的位置
            int left = 0, right = size;
            while (left < right) {
                int mid = left + (right - left) / 2;
                if (tails[mid] < num) {
                    left = mid + 1;
                } else {
                    right = mid;
                }
            }
            
            // 更新或追加
            tails[left] = num;
            if (left == size) {
                size++;
            }
        }
        
        return size;
    }
    
    // 测试方法
    public static void main(String[] args) {
        LongestIncreasingSubsequence solution = new LongestIncreasingSubsequence();
        
        int[] nums = {10, 9, 2, 5, 3, 7, 101, 18};
        
        System.out.println("输入数组: " + Arrays.toString(nums));
        System.out.println("最长递增子序列长度(DP): " + solution.lengthOfLIS(nums));
        System.out.println("最长递增子序列长度(优化): " + solution.lengthOfLISOptimized(nums));
    }
}

⏱️ 复杂度分析

动态规划

  • 时间复杂度:O(n²) - 双重循环
  • 空间复杂度:O(n) - dp 数组

贪心 + 二分

  • 时间复杂度:O(n log n) - 遍历一次,每次二分查找 O(log n)
  • 空间复杂度:O(n) - tails 数组

第23题:最长公共子序列

📝 题目描述

给定两个字符串 text1text2,返回这两个字符串的最长公共子序列的长度。

一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

示例

输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace",它的长度为 3。

输入:text1 = "abc", text2 = "abc"
输出:3

输入:text1 = "abc", text2 = "def"
输出:0

解题思路

二维动态规划

  1. 状态定义dp[i][j] 表示 text1[0..i-1]text2[0..j-1] 的最长公共子序列长度
  2. 状态转移方程
    如果 text1[i-1] == text2[j-1]:
        dp[i][j] = dp[i-1][j-1] + 1
    否则:
        dp[i][j] = max(dp[i-1][j], dp[i][j-1])
    
  3. 初始条件dp[0][j] = 0, dp[i][0] = 0
  4. 计算顺序:从左到右,从上到下

🎯 日常应用

  • 版本控制:Git 的差异比较(diff)
  • 文本比对:文档相似度检测
  • 生物信息学:DNA 序列比对
  • 抄袭检测:代码或文章相似度

🔧 代码解决的问题

  • 字符串匹配问题
  • 二维 DP 的应用
  • 序列比对

💻 Java实现代码

public class LongestCommonSubsequence {
    
    /**
     * 最长公共子序列 - 二维动态规划
     * @param text1 第一个字符串
     * @param text2 第二个字符串
     * @return 最长公共子序列的长度
     */
    public int longestCommonSubsequence(String text1, String text2) {
        int m = text1.length();
        int n = text2.length();
        
        // dp[i][j] 表示 text1[0..i-1] 和 text2[0..j-1] 的 LCS 长度
        int[][] dp = new int[m + 1][n + 1];
        
        // 状态转移
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
                    // 字符相同,LCS 长度 +1
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    // 字符不同,取较大值
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        
        return dp[m][n];
    }
    
    /**
     * 获取最长公共子序列(扩展)
     */
    public String getLCS(String text1, String text2) {
        int m = text1.length();
        int n = text2.length();
        
        int[][] dp = new int[m + 1][n + 1];
        
        // 填充 dp 表
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        
        // 回溯找到 LCS
        StringBuilder lcs = new StringBuilder();
        int i = m, j = n;
        while (i > 0 && j > 0) {
            if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
                lcs.append(text1.charAt(i - 1));
                i--;
                j--;
            } else if (dp[i - 1][j] > dp[i][j - 1]) {
                i--;
            } else {
                j--;
            }
        }
        
        return lcs.reverse().toString();
    }
    
    // 测试方法
    public static void main(String[] args) {
        LongestCommonSubsequence solution = new LongestCommonSubsequence();
        
        String text1 = "abcde";
        String text2 = "ace";
        
        System.out.println("text1: " + text1);
        System.out.println("text2: " + text2);
        System.out.println("LCS 长度: " + solution.longestCommonSubsequence(text1, text2));
        System.out.println("LCS: " + solution.getLCS(text1, text2));
    }
}

⏱️ 复杂度分析

  • 时间复杂度:O(m × n)

    • m 和 n 分别是两个字符串的长度
    • 需要填充 m × n 的 DP 表
  • 空间复杂度:O(m × n)

    • DP 表的大小为 (m+1) × (n+1)
    • 可优化到 O(min(m, n))

第24题:背包问题(0-1背包)

📝 题目描述

n 个物品和一个容量为 W 的背包。每个物品只能使用一次。

i 个物品的重量是 weight[i],价值是 value[i]

求解将哪些物品装入背包可使这些物品的总重量不超过背包容量,且总价值最大。

示例

输入:weight = [1, 3, 4], value = [15, 20, 30], W = 4
输出:35
解释:选择物品1(重量1,价值15)和物品3(重量3,价值20),
      总重量4,总价值35

💡 解题思路

动态规划

  1. 状态定义dp[i][j] 表示前 i 个物品,背包容量为 j 时的最大价值
  2. 状态转移方程
    如果不选第 i 个物品:
        dp[i][j] = dp[i-1][j]
    
    如果选第 i 个物品(j >= weight[i-1]):
        dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i-1]] + value[i-1])
    
  3. 初始条件dp[0][j] = 0, dp[i][0] = 0

空间优化

  • 使用一维数组 dp[j]
  • 注意:内层循环必须从后向前遍历(避免重复使用物品)

🎯 日常应用

  • 资源分配:预算最优分配
  • 投资组合:选择最优投资组合
  • 物流装载:货物装载优化
  • 项目管理:任务优先级排序

代码解决的问题

  • 最优化问题
  • 资源约束下的最优选择
  • 0-1 决策问题

💻 Java实现代码

public class Knapsack01 {
    
    /**
     * 0-1 背包问题 - 二维 DP
     * @param weight 物品重量数组
     * @param value 物品价值数组
     * @param W 背包容量
     * @return 最大价值
     */
    public int knapsack2D(int[] weight, int[] value, int W) {
        int n = weight.length;
        int[][] dp = new int[n + 1][W + 1];
        
        // 状态转移
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <= W; j++) {
                // 不选第 i 个物品
                dp[i][j] = dp[i - 1][j];
                
                // 选第 i 个物品(如果容量允许)
                if (j >= weight[i - 1]) {
                    dp[i][j] = Math.max(dp[i][j], 
                        dp[i - 1][j - weight[i - 1]] + value[i - 1]);
                }
            }
        }
        
        return dp[n][W];
    }
    
    /**
     * 0-1 背包问题 - 一维 DP(空间优化)
     */
    public int knapsack1D(int[] weight, int[] value, int W) {
        int n = weight.length;
        int[] dp = new int[W + 1];
        
        // 遍历物品
        for (int i = 0; i < n; i++) {
            // 从后向前遍历背包容量(关键!)
            for (int j = W; j >= weight[i]; j--) {
                dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
            }
        }
        
        return dp[W];
    }
    
    /**
     * 获取选择的物品(扩展)
     */
    public int[] getSelectedItems(int[] weight, int[] value, int W) {
        int n = weight.length;
        int[][] dp = new int[n + 1][W + 1];
        
        // 填充 DP 表
        for (int i = 1; i <= n; i++) {
            for (int j = 0; j <= W; j++) {
                dp[i][j] = dp[i - 1][j];
                if (j >= weight[i - 1]) {
                    dp[i][j] = Math.max(dp[i][j], 
                        dp[i - 1][j - weight[i - 1]] + value[i - 1]);
                }
            }
        }
        
        // 回溯找到选择的物品
        int[] selected = new int[n];
        int j = W;
        for (int i = n; i > 0 && j > 0; i--) {
            if (dp[i][j] != dp[i - 1][j]) {
                selected[i - 1] = 1; // 选择了第 i 个物品
                j -= weight[i - 1];
            }
        }
        
        return selected;
    }
    
    // 测试方法
    public static void main(String[] args) {
        Knapsack01 solution = new Knapsack01();
        
        int[] weight = {1, 3, 4, 5};
        int[] value = {15, 20, 30, 35};
        int W = 7;
        
        System.out.println("物品重量: " + java.util.Arrays.toString(weight));
        System.out.println("物品价值: " + java.util.Arrays.toString(value));
        System.out.println("背包容量: " + W);
        System.out.println("最大价值(二维DP): " + solution.knapsack2D(weight, value, W));
        System.out.println("最大价值(一维DP): " + solution.knapsack1D(weight, value, W));
        
        int[] selected = solution.getSelectedItems(weight, value, W);
        System.out.println("选择的物品: " + java.util.Arrays.toString(selected));
    }
}

⏱️ 复杂度分析

二维 DP

  • 时间复杂度:O(n × W)
  • 空间复杂度:O(n × W)

一维 DP

  • 时间复杂度:O(n × W)
  • 空间复杂度:O(W) - 大幅优化

第25题:编辑距离

题目描述

给你两个单词 word1word2,请你计算出将 word1 转换成 word2 所使用的最少操作数。

你可以对一个单词进行如下三种操作:

  1. 插入一个字符
  2. 删除一个字符
  3. 替换一个字符

示例

输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

输入:word1 = "intention", word2 = "execution"
输出:5

💡 解题思路

二维动态规划

  1. 状态定义dp[i][j] 表示 word1[0..i-1] 转换成 word2[0..j-1] 的最少操作数
  2. 状态转移方程
    如果 word1[i-1] == word2[j-1]:
        dp[i][j] = dp[i-1][j-1]  # 不需要操作
    否则:
        dp[i][j] = min(
            dp[i-1][j] + 1,      # 删除
            dp[i][j-1] + 1,      # 插入
            dp[i-1][j-1] + 1     # 替换
        )
    
  3. 初始条件
    • dp[i][0] = i(删除 i 个字符)
    • dp[0][j] = j(插入 j 个字符)

🎯 日常应用

  • 拼写检查:自动纠错
  • 搜索引擎:查询建议(“你是不是要找…”)
  • DNA 比对:基因序列差异
  • 版本控制:文件差异计算

🔧 代码解决的问题

  • 字符串编辑问题
  • 最短编辑路径
  • 字符串相似度计算

💻 Java实现代码

public class EditDistance {
    
    /**
     * 编辑距离 - 二维动态规划
     * @param word1 源字符串
     * @param word2 目标字符串
     * @return 最少操作数
     */
    public int minDistance(String word1, String word2) {
        int m = word1.length();
        int n = word2.length();
        
        // dp[i][j] 表示 word1[0..i-1] 转换成 word2[0..j-1] 的最少操作数
        int[][] dp = new int[m + 1][n + 1];
        
        // 初始条件
        for (int i = 0; i <= m; i++) {
            dp[i][0] = i; // 删除 i 个字符
        }
        for (int j = 0; j <= n; j++) {
            dp[0][j] = j; // 插入 j 个字符
        }
        
        // 状态转移
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    // 字符相同,不需要操作
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    // 取三种操作的最小值
                    dp[i][j] = Math.min(
                        Math.min(dp[i - 1][j] + 1,      // 删除
                                 dp[i][j - 1] + 1),     // 插入
                        dp[i - 1][j - 1] + 1            // 替换
                    );
                }
            }
        }
        
        return dp[m][n];
    }
    
    /**
     * 获取编辑路径(扩展)
     */
    public String getEditPath(String word1, String word2) {
        int m = word1.length();
        int n = word2.length();
        
        int[][] dp = new int[m + 1][n + 1];
        
        // 填充 DP 表
        for (int i = 0; i <= m; i++) dp[i][0] = i;
        for (int j = 0; j <= n; j++) dp[0][j] = j;
        
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    dp[i][j] = Math.min(
                        Math.min(dp[i - 1][j], dp[i][j - 1]),
                        dp[i - 1][j - 1]
                    ) + 1;
                }
            }
        }
        
        // 回溯找到编辑路径
        StringBuilder path = new StringBuilder();
        int i = m, j = n;
        while (i > 0 || j > 0) {
            if (i > 0 && j > 0 && word1.charAt(i - 1) == word2.charAt(j - 1)) {
                path.append("保持 '").append(word1.charAt(i - 1)).append("'\n");
                i--;
                j--;
            } else if (i > 0 && dp[i][j] == dp[i - 1][j] + 1) {
                path.append("删除 '").append(word1.charAt(i - 1)).append("'\n");
                i--;
            } else if (j > 0 && dp[i][j] == dp[i][j - 1] + 1) {
                path.append("插入 '").append(word2.charAt(j - 1)).append("'\n");
                j--;
            } else {
                path.append("替换 '").append(word1.charAt(i - 1))
                    .append("' 为 '").append(word2.charAt(j - 1)).append("'\n");
                i--;
                j--;
            }
        }
        
        return path.reverse().toString();
    }
    
    // 测试方法
    public static void main(String[] args) {
        EditDistance solution = new EditDistance();
        
        String word1 = "horse";
        String word2 = "ros";
        
        System.out.println("word1: " + word1);
        System.out.println("word2: " + word2);
        System.out.println("编辑距离: " + solution.minDistance(word1, word2));
        System.out.println("\n编辑路径:");
        System.out.println(solution.getEditPath(word1, word2));
    }
}

️ 复杂度分析

  • 时间复杂度:O(m × n)

    • m 和 n 分别是两个字符串的长度
    • 需要填充 m × n 的 DP 表
  • 空间复杂度:O(m × n)

    • DP 表的大小为 (m+1) × (n+1)
    • 可优化到 O(min(m, n))

📊 总结与对比

动态规划技巧总结

题目 状态定义 状态转移方程 时间复杂度 空间复杂度
爬楼梯 dp[i]: 到第i阶的方法数 dp[i] = dp[i-1] + dp[i-2] O(n) O(1)
最长递增子序列 dp[i]: 以nums[i]结尾的LIS dp[i] = max(dp[j]) + 1 O(n²)/O(n log n) O(n)
最长公共子序列 dp[i][j]: text1[0…i]和text2[0…j]的LCS 见上方 O(mn) O(mn)
0-1背包 dp[i][j]: 前i个物品容量j的最大价值 见上方 O(nW) O(W)
编辑距离 dp[i][j]: word1[0…i]到word2[0…j]的最少操作 见上方 O(mn) O(mn)

动态规划解题模板

1. 一维 DP 模板
int[] dp = new int[n + 1];
dp[0] = base_case;

for (int i = 1; i <= n; i++) {
    dp[i] = 状态转移方程;
}

return dp[n];
2. 二维 DP 模板
int[][] dp = new int[m + 1][n + 1];

// 初始化边界
for (int i = 0; i <= m; i++) dp[i][0] = base_case;
for (int j = 0; j <= n; j++) dp[0][j] = base_case;

// 状态转移
for (int i = 1; i <= m; i++) {
    for (int j = 1; j <= n; j++) {
        dp[i][j] = 状态转移方程;
    }
}

return dp[m][n];
3. 背包问题模板
int[] dp = new int[W + 1];

for (int item : items) {
    for (int j = W; j >= weight; j--) {  // 从后向前
        dp[j] = Math.max(dp[j], dp[j - weight] + value);
    }
}

return dp[W];

动态规划优化技巧

  1. 空间优化

    • 一维 DP → 常数空间(只保留必要状态)
    • 二维 DP → 一维 DP(滚动数组)
  2. 状态压缩

    • 使用位运算表示状态
    • 减少状态维度
  3. 记忆化搜索

    • 递归 + 缓存
    • 自顶向下的 DP

📚 下一篇预告

第6篇:搜索与排序(26-30)

  • 二分查找
  • 快速排序
  • 归并排序
  • 岛屿数量(DFS/BFS)
  • 全排列(回溯)

如果觉得这篇文章对您有帮助,欢迎:

  • 👍 点赞支持
  • 💬 留言讨论
  • ⭐ 收藏备用

有任何算法问题,欢迎在评论区交流!

更多推荐