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) 过来。
相关推荐
DeepModel2 小时前
【概率分布】几何分布超详细解析
算法·概率论
Genevieve_xiao2 小时前
【差分】差分的理解与基础题型总结
数据结构·c++·算法
舟舟亢亢2 小时前
算法总结——【技巧,ACM模式输入】
算法
智者知已应修善业2 小时前
【无序数组指针交换2则】2024-10-28
c语言·数据结构·c++·经验分享·笔记·算法
一叶落4382 小时前
LeetCode 136. 只出现一次的数字(C语言详解 | 哈希表 + 排序 + 位运算)
c语言·数据结构·算法·leetcode·哈希算法·散列表
古译汉书2 小时前
【数据结构算法】二分查找
c语言·开发语言·数据结构·c++·算法
逆境不可逃2 小时前
【从零入门23种设计模式19】行为型之观察者模式
java·开发语言·算法·观察者模式·leetcode·设计模式·动态规划
小龙报2 小时前
【算法通关指南:算法基础篇】二分答案专题:1.木材加工 2.砍树
c语言·数据结构·c++·算法·启发式算法
cici158742 小时前
经典的基于策略迭代和值迭代法的动态规划MATLAB实现
算法·matlab·动态规划