LeetCode 热题 100 之 152. 乘积最大子数组 416. 分割等和子集 32. 最长有效括号 62. 不同路径

本次的题目

  1. 乘积最大子数组

  2. 分割等和子集

  3. 最长有效括号

  4. 不同路径

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:动态规划
  1. 预处理

    1. 先计算数组所有元素的和 sum。如果 sum 是奇数,直接返回 false,因为无法分成两个相等的整数和。

    2. 目标和 target = sum / 2

  2. 动态规划定义

    1. dp[i] 表示是否可以凑出和为 i 的子集。

    2. 初始化:dp[0] = true,因为和为 0 总是可以通过不选任何元素实现。

  3. 状态转移

    1. 遍历数组中的每个数 num

    2. targetnum 逆序遍历,更新 dp[j] = dp[j] || dp[j - num]

  4. 结果

    1. 最终 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 符合预期。

总结
  1. 当 num 大于 target 时,内层循环会直接跳过(因为 j >= num 不成立),不会影响 dp 数组;

  2. 只要在遍历过程中 dp [target] 变为 true,就会提前返回,否则遍历结束后返回 dp [target];

  3. 不同测试用例的核心差异在于:是否能通过 "选 / 不选" 数字,让 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] + 2i≥2 时取 dp[i-2],否则直接加 2)。

      • 情况 2:前一个字符是 ')' → 检查「前一个有效子串的前一位」是否是 '(',若是则 dp[i] = dp[i-1] + dp[i-dp[i-1]-2] + 2i-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) 过来。
相关推荐
一只幸运猫.4 小时前
2026Java 后端面试完整版|八股简答 + AI 大模型集成技术(最新趋势)
人工智能·面试·职场和发展
Old Uncle Tom4 小时前
OpenClaw 记忆系统 -- 记忆预加载
java·数据结构·算法·agent
会编程的土豆4 小时前
洛谷题单入门1 顺序结构
数据结构·算法·golang
生信碱移4 小时前
PACells:这个方法可以鉴定疾病/预后相关的重要细胞亚群,作者提供的代码流程可以学习起来了,甚至兼容转录组与 ATAC 两种数据类型!
人工智能·学习·算法·机器学习·数据挖掘·数据分析·r语言
智者知已应修善业5 小时前
【51单片机中的打飞机设计】2023-8-25
c++·经验分享·笔记·算法·51单片机
圣保罗的大教堂6 小时前
leetcode 1855. 下标对中的最大距离 中等
leetcode
智者知已应修善业7 小时前
【51单片机按键调节占空比3位数码管显示】2023-8-24
c++·经验分享·笔记·算法·51单片机
.5488 小时前
## Sorting(排序算法)
python·算法·排序算法
wuweijianlove8 小时前
算法的平均复杂度建模与性能回归分析的技术7
算法·数据挖掘·回归
子琦啊8 小时前
【算法复习】字符串 | 两个底层直觉,吃透高频题
linux·运维·算法