本次的题目
乘积最大子数组
分割等和子集
最长有效括号
不同路径
152. 乘积最大子数组

public class Solution {
public int maxProduct(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int max = nums[0];
int curMax = nums[0];
int curMin = nums[0];
for (int i = 1; i < nums.length; i++) {
int temp = curMax;
if (nums[i] < 0) {
// 负数交换最大最小
int tmp = curMax;
curMax = curMin;
curMin = tmp;
}
// 更新当前最大和最小
curMax = Math.max(nums[i], curMax * nums[i]);
curMin = Math.min(nums[i], curMin * nums[i]);
max = Math.max(max, curMax);
}
return max;
}
}
解题思路1:动态规划
-
定义:
-
maxDp[i]:以nums[i]结尾的子数组的最大乘积 -
minDp[i]:以nums[i]结尾的子数组的最小乘积
-
-
转移方程:
-
当
nums[i] >= 0时:-
maxDp[i] = max(maxDp[i-1] * nums[i], nums[i]) -
minDp[i] = min(minDp[i-1] * nums[i], nums[i])
-
-
当
nums[i] < 0时:-
maxDp[i] = max(minDp[i-1] * nums[i], nums[i]) -
minDp[i] = min(maxDp[i-1] * nums[i], nums[i])
-
-
-
最终答案是所有
maxDp中的最大值。
416. 分割等和子集
这是 LeetCode 第 416 题「分割等和子集」,本质是一个0-1 背包问题:判断是否能从数组中选出一些数,使它们的和等于总和的一半。

public class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for (int num : nums) {
sum += num;
}
// 和为奇数,直接返回false
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。如果sum是奇数,直接返回false,因为无法分成两个相等的整数和。 -
目标和
target = sum / 2。
-
-
动态规划定义:
-
dp[i]表示是否可以凑出和为i的子集。 -
初始化:
dp[0] = true,因为和为 0 总是可以通过不选任何元素实现。
-
-
状态转移:
-
遍历数组中的每个数
num。 -
从
target到num逆序遍历,更新dp[j] = dp[j] || dp[j - num]。
-
-
结果:
- 最终
dp[target]的值即为答案。
- 最终
测试用例:nums = [1,2,3,4,5,6,7]
步骤 1:初始化计算
int sum = 1+2+3+4+5+6+7 = 28; // 偶数
int target = 28 / 2 = 14;
boolean[] dp = new boolean[15]; // 索引0~14
dp[0] = true; // 初始dp = [true, false, false, ..., false]
步骤 2:遍历第一个数字 num = 1
内层逆序遍历 j=14 → 1:
-
只有 j=1 时,dp [1] = false || dp [0] = true
-
其余 j 均为 false
-
此时 dp = [true, true, false, false, false, false, false, false, false, false, false, false, false, false, false]
步骤 3:遍历第二个数字 num = 2
内层逆序遍历 j=14 → 2:
-
j=2:dp[2] = false || dp[0] = true
-
j=3:dp[3] = false || dp[1] = true
-
其余 j(4~14):false
-
此时 dp = [true, true, true, true, false, false, false, false, false, false, false, false, false, false, false]
步骤 4:遍历第三个数字 num = 3
内层逆序遍历 j=14 → 3:
-
j=3:dp [3] = true || dp [0] = true(不变)
-
j=4:dp[4] = false || dp[1] = true
-
j=5:dp[5] = false || dp[2] = true
-
j=6:dp[6] = false || dp[3] = true
-
其余 j(7~14):false
-
此时 dp = [true, true, true, true, true, true, true, false, false, false, false, false, false, false, false]
步骤 5:遍历第四个数字 num = 4
内层逆序遍历 j=14 → 4:
-
j=4:dp [4] = true || dp [0] = true(不变)
-
j=5:dp [5] = true || dp [1] = true(不变)
-
j=6:dp [6] = true || dp [2] = true(不变)
-
j=7:dp[7] = false || dp[3] = true
-
j=8:dp[8] = false || dp[4] = true
-
j=9:dp[9] = false || dp[5] = true
-
j=10:dp[10] = false || dp[6] = true
-
其余 j(11~14):false
-
此时 dp = [true, true, true, true, true, true, true, true, true, true, true, false, false, false, false]
步骤 6:遍历第五个数字 num = 5
内层逆序遍历 j=14 → 5:
-
j=5:dp [5] = true || dp [0] = true(不变)
-
j=6:dp [6] = true || dp [1] = true(不变)
-
...(j=7~10 均不变)
-
j=11:dp[11] = false || dp[6] = true
-
j=12:dp[12] = false || dp[7] = true
-
j=13:dp[13] = false || dp[8] = true
-
j=14:dp[14] = false || dp[9] = true → true
-
触发
if (dp[14]),直接返回true,代码结束!
结果验证
这个测试用例中,数组总和 28,目标和 14,确实能找到子集(比如 [1,6,7]、[2,5,7]、[3,4,7] 等),代码返回 true 符合预期。
总结
-
当 num 大于 target 时,内层循环会直接跳过(因为 j >= num 不成立),不会影响 dp 数组;
-
只要在遍历过程中 dp [target] 变为 true,就会提前返回,否则遍历结束后返回 dp [target];
-
不同测试用例的核心差异在于:是否能通过 "选 / 不选" 数字,让 dp [target] 最终变为 true。
32. 最长有效括号

class Solution {
public int longestValidParentheses(String s) {
// 1. 预处理:长度不足2直接返回0
if (s == null || s.length() < 2) {
return 0;
}
int n = s.length();
int maxLen = 0;
// 2. 栈定义 & 初始化:存储索引,-1为初始基准
Deque<Integer> stack = new LinkedList<>();
stack.push(-1);
// 3. 状态转移(遍历更新)
for (int i = 0; i < n; i++) {
char c = s.charAt(i);
if (c == '(') {
// 左括号:压入索引,等待匹配
stack.push(i);
} else {
// 右括号:弹出栈顶(尝试匹配)
stack.pop();
if (stack.isEmpty()) {
// 栈空:更新基准位为当前索引
stack.push(i);
} else {
// 计算有效长度,更新最大值
maxLen = Math.max(maxLen, i - stack.peek());
}
}
}
// 4. 返回结果
return maxLen;
}
}
解题思路1:栈
预处理
-
输入校验:若字符串长度 < 2,直接返回 0。
-
目标:通过栈记录「未匹配括号的索引」,利用索引差计算有效长度,找到最大值。
栈的定义与初始化
-
栈存储括号索引,用于标记「有效括号的起始基准位」。
-
初始化:压入
-1作为初始基准(处理第一个字符是')'的边界情况)。 -
辅助变量:
maxLen记录最长有效长度,初始为 0。
状态转移(遍历更新)
-
遍历字符串每个字符(索引
i):-
若当前字符是
'(':将索引i压入栈(等待匹配)。 -
若当前字符是
')':-
弹出栈顶元素(表示尝试匹配最近的左括号)。
-
若栈为空:将当前索引
i压入栈(更新新的基准位)。 -
若栈非空:计算当前有效长度
i - 栈顶元素,更新maxLen。
-
-
结果
-
最终返回
maxLen。class Solution {
public int longestValidParentheses(String s) {
// 1. 预处理:长度不足2直接返回0
if (s == null || s.length() < 2) {
return 0;
}
int n = s.length();
int maxLen = 0;// 2. 动态规划定义 & 初始化:dp[i]表示以i结尾的最长有效括号长度 int[] dp = new int[n]; // 初始值全为0 // 3. 状态转移 for (int i = 1; i < n; i++) { // 仅处理右括号结尾的情况(左括号结尾无有效长度) if (s.charAt(i) == ')') { // 情况1:前一个字符是左括号,直接匹配 if (s.charAt(i - 1) == '(') { dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2; } // 情况2:前一个字符是右括号,检查内部有效子串的前一位是否是左括号 else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') { dp[i] = dp[i - 1] + ((i - dp[i - 1] >= 2) ? dp[i - dp[i - 1] - 2] : 0) + 2; } // 更新最大长度 maxLen = Math.max(maxLen, dp[i]); } } // 4. 返回结果 return maxLen;}
}
解题思路2:动态规划(HARD)
预处理
-
输入校验:若字符串长度 < 2,直接返回 0(单个字符不可能构成有效括号)。
-
目标:找到字符串中最长的有效括号子串长度(无显式 sum/target,核心是推导每个位置的最长有效长度)。
动态规划定义
-
dp[i]:表示以字符串第i个字符(索引为i)结尾的最长有效括号子串长度。 -
初始化:
dp数组长度等于字符串长度,所有元素初始化为 0(默认无有效长度)。
状态转移
-
遍历字符串(从索引 1 开始,因为单个字符无意义):
-
若当前字符是
')':-
情况 1:前一个字符是
'('→ 直接匹配,dp[i] = dp[i-2] + 2(i≥2时取dp[i-2],否则直接加 2)。 -
情况 2:前一个字符是
')'→ 检查「前一个有效子串的前一位」是否是'(',若是则dp[i] = dp[i-1] + dp[i-dp[i-1]-2] + 2(i-dp[i-1]-2 ≥0时取dp[i-dp[i-1]-2],否则加 0)。
-
-
若当前字符是
'(':dp[i]保持 0(以'('结尾不可能是有效括号)。
-
-
过程中持续更新「最大有效长度」。
结果
- 最终返回遍历过程中记录的「最大有效长度」。
62. 不同路径

public class Solution {
public int uniquePaths(int m, int n) {
// 为了计算简便,我们计算 C(m+n-2, min(m-1, n-1))
int a = m - 1;
int b = n - 1;
int total = a + b;
int k = Math.min(a, b);
long result = 1;
for (int i = 1; i <= k; i++) {
result = result * (total - k + i) / i;
}
return (int) result;
}
}
解题思路1:组合数学
要从 m x n 网格的左上角走到右下角,机器人只能向右或向下移动。
-
总共需要向下走
m-1步 -
总共需要向右走
n-1步 -
总步数为
(m-1) + (n-1) = m + n - 2步
问题转化为:在 m + n - 2 步中,选择 m-1 步向下(或 n-1 步向右)的组合数。
公式为:
public class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
// 初始化第一行
for (int j = 0; j < n; j++) {
dp[0][j] = 1;
}
// 初始化第一列
for (int i = 0; i < m; i++) {
dp[i][0] = 1;
}
// 填充 dp 表
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
解题思路2:动态规划
我们定义 dp[i][j] 为从起点 (0, 0) 走到 (i, j) 的路径数。
-
边界条件:
-
第一行
dp[0][j] = 1,因为只能一直向右走。 -
第一列
dp[i][0] = 1,因为只能一直向下走。
-
-
状态转移方程:
dp[i][j] = dp[i-1][j] + dp[i][j-1],因为到达(i, j)只能从上方(i-1, j)或左方(i, j-1)过来。
