LeetCode 热题 100 精讲 | 动态规划进阶篇:最大子数组和 · 分割等和子集 · 最长公共子序列 · 打家劫舍 III

一、53. 最大子数组和

🔗 题目链接

LeetCode 53. 最大子数组和

📝 题目描述

给你一个整数数组 nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例

复制代码
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6。

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

🧠 思路分析

最大子数组和是动态规划的经典入门题。定义 dp[i] 表示以 nums[i] 结尾的连续子数组的最大和。对于每个位置 i,要么把 nums[i] 接在 dp[i-1] 后面继续累加,要么以 nums[i] 自己作为新子数组的开头,取两者中较大的那个,即 dp[i] = max(nums[i], dp[i-1] + nums[i])。最终答案就是 dp 数组中的最大值。这里也可以用一个变量滚动记录,把空间复杂度降到 O(1)。

💻 代码实现(C++)

cpp 复制代码
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int cur = nums[0];
        int maxSum = nums[0];
        for (int i = 1; i < nums.size(); i++) {
            cur = max(nums[i], cur + nums[i]);
            maxSum = max(maxSum, cur);
        }
        return maxSum;
    }
};

📚 相关学习资源

免责声明:以上链接均来自公开网络。若存在侵权问题,请联系删除。

⏱ 复杂度分析

  • 时间复杂度:O(n),一次遍历。

  • 空间复杂度:O(1),只用常数个变量。


二、416. 分割等和子集

🔗 题目链接

LeetCode 416. 分割等和子集

📝 题目描述

给你一个只包含正整数的非空数组 nums,判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

示例

复制代码
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11]。

输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。

🧠 思路分析

这道题的关键在于将其转化为 0-1 背包问题。先计算数组的总和,如果总和是奇数,直接返回 false;如果总和是偶数,那么目标就是判断能否从数组中选出一些元素,使它们的和等于总和的一半。这就相当于一个背包容量为 sum/2 的 0-1 背包问题,每个元素只能选一次,问能否恰好装满背包。定义 dp[j] 表示能否凑出总和 j,状态转移方程为 dp[j] = dp[j] || dp[j - nums[i]]。外层循环遍历每个元素,内层循环从 sum/2 倒序遍历到 nums[i],这样可以保证每个元素只被使用一次。

💻 代码实现(C++)

cpp 复制代码
class Solution {
public:
    bool canPartition(vector<int>& nums) {
        int sum = 0;
        for (int num : nums) sum += num;
        if (sum % 2 != 0) return false;
        int target = sum / 2;
        vector<bool> dp(target + 1, false);
        dp[0] = true;
        for (int num : nums) {
            for (int j = target; j >= num; j--) {
                dp[j] = dp[j] || dp[j - num];
            }
        }
        return dp[target];
    }
};

📚 相关学习资源

免责声明:以上链接均来自公开网络。若存在侵权问题,请联系删除。

⏱ 复杂度分析

  • 时间复杂度:O(n × target),其中 target = sum/2。

  • 空间复杂度:O(target),dp 数组的大小。


三、1143. 最长公共子序列

🔗 题目链接

LeetCode 1143. 最长公共子序列

📝 题目描述

给定两个字符串 text1text2,返回这两个字符串的最长公共子序列的长度。子序列是指在不改变字符相对顺序的情况下删除某些字符后形成的新字符串。

示例

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

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

🧠 思路分析

定义 dp[i][j] 表示 text1 的前 i 个字符和 text2 的前 j 个字符的最长公共子序列长度。当 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])。初始状态 dp[0][j] = 0dp[i][0] = 0,最后返回 dp[m][n]。这个二维 DP 表格的填充过程就是不断比较两个字符串的字符,逐步构建出最优解。

💻 代码实现(C++)

cpp 复制代码
class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.size(), n = text2.size();
        vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (text1[i - 1] == text2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[m][n];
    }
};

📚 相关学习资源

免责声明:以上链接均来自公开网络。若存在侵权问题,请联系删除。

⏱ 复杂度分析

  • 时间复杂度:O(m × n),m 和 n 分别是两个字符串的长度。

  • 空间复杂度:O(m × n),二维 dp 数组的大小。


四、337. 打家劫舍 III

🔗 题目链接

LeetCode 337. 打家劫舍 III

📝 题目描述

在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为"根"。除了"根"之外,每栋房子有且只有一个"父"房子与之相连。如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。

示例

复制代码
输入:root = [3,2,3,null,3,null,1]
输出:7
解释:小偷一晚能盗取的最高金额 = 3 + 3 + 1 = 7。

🧠 思路分析

这道题是树形 DP 的经典题。对于每个节点,我们考虑两种状态:偷当前节点或不偷当前节点。用后序遍历的方式,先处理左子树和右子树,再处理当前节点。定义递归函数返回一个长度为 2 的数组 dpdp[0] 表示不偷当前节点时该子树能获得的最大金额,dp[1] 表示偷当前节点时该子树能获得的最大金额。状态转移:不偷当前节点时,左右子节点可以偷也可以不偷,取最大值相加;偷当前节点时,左右子节点都不能偷,取左右子节点不偷的值加上当前节点的值。最终结果就是根节点两种状态的最大值。

💻 代码实现(C++)

cpp 复制代码
class Solution {
public:
    int rob(TreeNode* root) {
        vector<int> res = dfs(root);
        return max(res[0], res[1]);
    }

    vector<int> dfs(TreeNode* node) {
        if (!node) return {0, 0};
        vector<int> left = dfs(node->left);
        vector<int> right = dfs(node->right);
        int notRob = max(left[0], left[1]) + max(right[0], right[1]);
        int rob = node->val + left[0] + right[0];
        return {notRob, rob};
    }
};

📚 相关学习资源

免责声明:以上链接均来自公开网络。若存在侵权问题,请联系删除。

⏱ 复杂度分析

  • 时间复杂度:O(n),每个节点被访问一次。

  • 空间复杂度:O(h),h 为树高,递归调用栈的深度。


结语

动态规划进阶篇的四道题代表了 DP 的不同应用场景:一维线性 DP (最大子数组和)、0-1 背包问题 (分割等和子集)、二维字符串 DP (最长公共子序列)、树形 DP(打家劫舍 III)。从一维数组到二维表格,从线性结构到树形结构,这四道题基本涵盖了面试中动态规划的高频考察方向。吃透这些题,动态规划的能力会上一个台阶。

建议刷题顺序:先做最大子数组和理解 DP 的基本状态定义,再做最长公共子序列熟悉二维 DP 表格的填充,然后用分割等和子集理解背包问题的建模能力,最后用打家劫舍 III 挑战树形 DP。下一篇将进入 动态规划高阶篇(编辑距离、最长回文子序列、股票系列),敬请期待。

如果本文对你有帮助,欢迎点赞、收藏、转发,你的支持是我持续创作的动力 ❤️

免责声明:本文部分解题思路参考了力扣官方题解及社区优秀文章,相关链接均来自公开网络。若存在侵权问题,请联系删除。

相关推荐
li1670902702 小时前
第十章:list
c语言·开发语言·数据结构·c++·算法·list·visual studio
‎ദ്ദിᵔ.˛.ᵔ₎2 小时前
仿函数使用
c++
Z1Jxxx2 小时前
C++ P1150 Peter 的烟
数据结构·c++·算法
是娇娇公主~2 小时前
线程池:工作窃取线程池WorkingStealingPool
c++·线程池
CheerWWW2 小时前
C++学习笔记——函数指针、Lambda表达式、谨慎使用using namespace std、命名空间
c++·笔记·学习
夜猫子ing2 小时前
如何编写一个CMakelists文件
开发语言·c++
踮起脚看烟花2 小时前
chapter10_泛型算法
c++·算法
笨笨饿2 小时前
# 52_浅谈为什么工程基本进入复数域?
linux·服务器·c语言·数据结构·人工智能·算法·学习方法
Code-keys2 小时前
ADSP/ARM 性能/稳定性排查专栏总述
arm开发·算法·边缘计算·dsp开发