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

前言

动态规划(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,可以直接返回,节省后续计算。
相关推荐
三品吉他手会点灯5 小时前
C语言学习笔记 - 33.数据类型 - printf函数的详细用法
c语言·开发语言·笔记·学习·算法
叶~小兮5 小时前
Kubernetes集群升级与证书更新 学习笔记
笔记·学习·kubernetes
NashSKY6 小时前
PnP 问题:数学描述与 DLT 算法推导
算法·矩阵分解·多视图几何·射影几何
csdn_aspnet6 小时前
C++ Lomuto分区算法(Lomuto Partition Algorithm)
开发语言·c++·算法
ZPC82106 小时前
Open3D 与yolo-3d 那个更适合生成物体3d 包围盒
人工智能·算法·计算机视觉·机器人
脆皮炸鸡7556 小时前
进程信号~信号的产生
linux·服务器·开发语言·经验分享·笔记·学习方法
行走的陀螺仪6 小时前
JavaScript 算法详解:10大经典算法,通俗易懂,从入门到精通
开发语言·javascript·算法
1368木林森6 小时前
RAG查询改写②【第十篇】:HYDE、StepBack、子问题拆分,高阶改写算法生产落地
人工智能·算法·rag
smj2302_796826527 小时前
解决leetcode第3934题最短唯一子数组
数据结构·python·算法·leetcode