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:dp2 = false || dp0 = true

  • j=3:dp3 = false || dp1 = 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:dp4 = false || dp1 = true

  • j=5:dp5 = false || dp2 = true

  • j=6:dp6 = false || dp3 = 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:dp7 = false || dp3 = true

  • j=8:dp8 = false || dp4 = true

  • j=9:dp9 = false || dp5 = true

  • j=10:dp10 = false || dp6 = 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:dp11 = false || dp6 = true

  • j=12:dp12 = false || dp7 = true

  • j=13:dp13 = false || dp8 = true

  • j=14:dp14 = false || dp9 = true → true

  • 触发 if (dp[14]),直接返回 true,代码结束!

结果验证

这个测试用例中,数组总和 28,目标和 14,确实能找到子集(比如 1,6,72,5,73,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) 过来。
相关推荐
兰令水1 小时前
leecodecode【面试150】【2026.6.14打卡-java版本】
java·算法·面试
noipp8 小时前
推荐题目:洛谷 P10907 [蓝桥杯 2024 国 B] 蚂蚁开会
c语言·c++·算法·编程·洛谷
程序员二叉8 小时前
【JUC】ThreadLocal底层原理|内存泄漏|弱引用|跨线程传递方案
java·开发语言·面试·职场和发展·juc
程序员二叉8 小时前
【JUC】线程池全套深度详解|参数|流程|拒绝策略|调优|异常处理
java·开发语言·jvm·算法·面试·juc
青山木9 小时前
Hot 100 --- 轮转数组
java·数据结构·算法
徐小夕9 小时前
Loop Engineering 深度解析与实战指南(全网最全)
前端·算法·github
北域码匠10 小时前
SHA-1算法:安全哈希原理与应用解析
算法·c#·哈希算法
手写码匠11 小时前
手写 GraphRAG:从零实现图增强检索增强生成系统
人工智能·深度学习·算法·aigc
BomanGe111 小时前
NSK重载高刚性滚珠丝杠技术详解
经验分享·算法·规格说明书
Matrix_1112 小时前
手机里的计算摄影:广角形变校正算法
人工智能·算法·智能手机·计算摄影