两道经典动态规划题:乘积最大子数组 & 分割等和子集 复盘笔记

前言

动态规划(DP)是算法面试里绕不开的重点,而这两道中等难度的题目,一个是连续子数组的最值问题 ,一个是0-1 背包的变形题,正好覆盖了 DP 里两种非常典型的场景。今天就把这两道题的思路、坑点和代码实现整理出来,方便以后复习。


一、152. 乘积最大子数组

题目描述

给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

解题思路

这道题和我们熟悉的「最大子数组和」非常像,但因为乘法的特殊性,不能直接照搬 "以 i 结尾的最大值" 这一套:

  • 两个负数相乘会变成正数,所以当前的最小值乘一个负数,反而可能变成最大值。
  • 比如数组 [-2, 3, -4],-2 * 3 = -6,但 (-2) * 3 * (-4) = 24,是最大值。

所以我们的 DP 状态不能只存 "以当前位置结尾的最大值",还要同时存 "以当前位置结尾的最小值":

  • dp_max[i]:以 nums[i] 结尾的子数组的最大乘积
  • dp_min[i]:以 nums[i] 结尾的子数组的最小乘积

状态转移方程:

text

复制代码
dp_max[i] = max(nums[i], dp_max[i-1] * nums[i], dp_min[i-1] * nums[i])
dp_min[i] = min(nums[i], dp_max[i-1] * nums[i], dp_min[i-1] * nums[i])

每次更新后,都用 dp_max[i] 去更新全局的最大值。

优化空间

因为每次只需要前一个状态的 dp_max 和 dp_min,所以可以不用开数组,只用两个变量滚动更新即可,空间复杂度从 O (n) 降到 O (1)。

完整代码(Java)

java

运行

复制代码
class Solution {
    public int maxProduct(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        int max = nums[0];
        int currMax = nums[0];
        int currMin = nums[0];
        for (int i = 1; i < nums.length; i++) {
            int tempMax = currMax;
            currMax = Math.max(nums[i], Math.max(currMax * nums[i], currMin * nums[i]));
            currMin = Math.min(nums[i], Math.min(tempMax * nums[i], currMin * nums[i]));
            max = Math.max(max, currMax);
        }
        return max;
    }
}

坑点总结

  1. 忘记负数情况:只存最大值会漏掉 "负负得正" 的场景,必须同时维护最大值和最小值。
  2. 临时变量覆盖问题 :更新 currMax 后,再更新 currMin 时不能用已经更新的 currMax,所以需要用临时变量保存旧值。
  3. 初始化问题 :max、currMax、currMin 都要初始化为 nums[0],不能初始化为 0,否则数组全是负数时会出错。

二、416. 分割等和子集

题目描述

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

解题思路

这道题本质上是一个 0-1 背包问题:

  • 首先,整个数组的和 sum 必须是偶数,否则直接返回 false(不可能分成两个和相等的子集)。
  • 问题转化为:是否能从数组中选出一些数,让它们的和等于 sum / 2。

我们定义 dp[i] 表示:是否能从数组中选出若干个数,它们的和为 i。

  • 初始状态:dp[0] = true(和为 0,不选任何数即可)。

  • 状态转移:遍历每个数 num,从后往前更新 dp 数组:

    text

    复制代码
    dp[j] = dp[j] || dp[j - num]

    表示 "选或不选当前数 num,能否凑成和为 j"。

优化空间

和标准 0-1 背包一样,我们可以用一维数组从后往前更新,避免重复使用同一个元素,空间复杂度为 O (n/2)。

完整代码(Java)

java

运行

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

坑点总结

  1. 和为奇数的情况 :没有提前判断 sum 是否为偶数,导致后续逻辑无效。
  2. 更新顺序错误:从前往后更新会变成 "完全背包",同一个元素会被多次使用,必须从后往前更新。
  3. 边界问题 :dp 数组的长度是 target + 1,不能是 target,否则会越界。
  4. 提前剪枝 :在遍历过程中,如果 dp[target] 已经为 true,可以直接返回,节省后续计算。
相关推荐
hanlin036 小时前
刷题笔记:力扣第32题-最长有效括号
笔记·算法·leetcode
All for pursuit.6 小时前
【动态规划-21】312.戳气球
数据结构·c++·算法·leetcode·动态规划
xian_wwq7 小时前
【学习笔记】Milvus 如何完成一次向量相似度检索,Milvus 索引到底应该怎么选?-15
笔记·学习·milvus
碧海银沙音频科技研究院9 小时前
基于炬芯ATS3089X自研AEC声学回声消除算法
算法
通问AI9 小时前
在两台 Mac Studio 上跑 800B 模型:MLX + RDMA over Thunderbolt 实测与四道门选型法
算法
ZywOo6239 小时前
《算法竞赛进阶指南》0x03 前缀和与差分
算法
Scintilla_y9 小时前
DAY2 滑动窗口
算法·leetcode·散列表
lvwangshu9 小时前
笛卡尔树
算法·图论
026d239 小时前
状态信号只知“其然”不知“其所以然”:变工况机械故障诊断的物理盲区
算法
姜鱼问生10 小时前
备份和服务器在同一台机器上,等于没备份
算法·架构