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

前言

动态规划(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_maxdp_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. 初始化问题maxcurrMaxcurrMin 都要初始化为 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,可以直接返回,节省后续计算。
相关推荐
m0_5474866616 小时前
《数据结构与算法》全套PPT课件2026(中国海洋大学)
数据结构·算法
旖旎夜光17 小时前
LeetCode 904:水果成篮(滑动窗口) —— 题解
数据结构·c++·算法·leetcode·滑动窗口
怪奇云呼军17 小时前
G.711、Opus 和重采样会拖慢识别吗?闪电智能VoiceAgent 的音频入口怎么选
java·人工智能·python·算法·云计算·音视频
乐观勇敢坚强的老彭18 小时前
C++ 竞赛常用算法模板速查表
开发语言·c++·算法
高洁0118 小时前
工信部教考中心证书
人工智能·深度学习·算法·机器学习·知识图谱
我变成萤火虫19 小时前
河南萌新联赛2026第(三)场:郑州轻工业大学
数据结构·c++·算法·贪心算法·stl·深度优先·哈希算法
Dr.kangder19 小时前
嵌入式面试总结(十九)——内存泄露
单片机·算法·面试·职场和发展·架构·硬件架构
Herbert_hwt19 小时前
【C语言基础】常量、选择结构与运算符全解析
c语言·开发语言·算法
demodeom19 小时前
Win11 + RTX 5080 本地 Coding 模型测试笔记(已弃坑)
笔记
Suxing919 小时前
C语言基础分享:从“租房”到“拆迁”,C语言动态内存管理
java·数据结构·算法