前言
动态规划(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;
}
}
坑点总结
- 忘记负数情况:只存最大值会漏掉 "负负得正" 的场景,必须同时维护最大值和最小值。
- 临时变量覆盖问题 :更新
currMax后,再更新currMin时不能用已经更新的currMax,所以需要用临时变量保存旧值。 - 初始化问题 :
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];
}
}
坑点总结
- 和为奇数的情况 :没有提前判断
sum是否为偶数,导致后续逻辑无效。 - 更新顺序错误:从前往后更新会变成 "完全背包",同一个元素会被多次使用,必须从后往前更新。
- 边界问题 :
dp数组的长度是target + 1,不能是target,否则会越界。 - 提前剪枝 :在遍历过程中,如果
dp[target]已经为true,可以直接返回,节省后续计算。