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

前言

动态规划(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,可以直接返回,节省后续计算。
相关推荐
青梅味猪大肠25 分钟前
【操作系统-60】临近适应算法
java·linux·开发语言·嵌入式硬件·算法·操作系统
FakeOccupational33 分钟前
【电路笔记 STM32】RTC配置:晶振选择+闹钟Configuration+中断设置
笔记·stm32·实时音视频
迪康coolmu37 分钟前
企业U盘管控落地实战:基于迪康端点安全一体化管理系统的移动存储安全实践
运维·人工智能·经验分享·笔记·安全·电脑
Z21-X3 小时前
云原生运维学习笔记:容器技术的演进、底层原理与 Docker 全生命周期管理
运维·笔记·学习·云原生
Rabitebla11 小时前
【Linux 底层原理】进程(十):进程等待详解---从僵尸进程到 wait、waitpid 与 status
linux·c++·笔记·学习·visual studio
小lu飞12 小时前
2026小红书小工具怎么制作:纯前端 ZIP 打包、沙箱限制与笔记挂载流程
前端·笔记·ai·ai编程
其实防守也摸鱼15 小时前
个人学习 Pwn 思路:从基础到漏洞利用的进阶路径
大数据·前端·人工智能·chrome·算法·安全·网络安全
Nuanyt16 小时前
LeetCode Hot 100 链表专题(五)· 2 题(Java)
java·算法·leetcode·链表
无心水16 小时前
24、系统架构师论文写作专题 核心考点与学习笔记
笔记·学习·系统架构·架构师·安全架构·软考架构师·软考系统架构设计师