动态规划 最长有效括号

LeetCode 32. 最长有效括号

问题描述

给定一个仅包含 '('')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

示例

示例 1:

输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"

示例 2:

输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"

示例 3:

输入:s = ""
输出:0

算法思路

动态规划(DP)

  1. 状态定义dp[i] 表示以 s[i] 结尾的最长有效括号长度
  2. 状态转移
    • s[i] = ')' 时:
      • s[i-1] = '('dp[i] = dp[i-2] + 2
      • s[i-1] = ')'s[i - dp[i-1] - 1] = '('
        dp[i] = dp[i-1] + 2 + dp[i - dp[i-1] - 2];
        
  3. 初始化dp[0] = 0
  4. 结果max(dp[i])

栈方法

  1. 栈底元素:始终存储最后一个未匹配的右括号位置(初始为 -1
  2. 遍历字符串
    • '(':压入当前索引
    • ')'
      • 弹出栈顶(匹配一个左括号)
      • 栈空:压入当前索引(作为新边界)
      • 栈非空:计算当前有效长度 = i - stack.peek()

代码实现

方法一:动态规划

class Solution {
    public int longestValidParentheses(String s) {
        int n = s.length();
        if (n < 2) return 0;
        
        // dp[i] 表示以 s.charAt(i) 结尾的最长有效括号长度
        int[] dp = new int[n];
        int maxLen = 0;
        
        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) == '(') {
                    // 核心公式:当前有效长度 = 内部有效长度 + 2 + 前面的有效长度
                    dp[i] = dp[i - 1] + 2;
                    if (i - dp[i - 1] - 2 >= 0) {
                        dp[i] += dp[i - dp[i - 1] - 2];
                    }
                }
                maxLen = Math.max(maxLen, dp[i]);
            }
            // 以 '(' 结尾的子串无效,dp[i] 保持为 0
        }
        return maxLen;
    }
}

方法二:栈方法(推荐)

class Solution {
    public int longestValidParentheses(String s) {
        Deque<Integer> stack = new ArrayDeque<>();
        stack.push(-1); // 初始边界,表示最后一个未匹配右括号位置
        int maxLen = 0;
        
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == '(') {
                // 遇到左括号,压入当前索引
                stack.push(i);
            } else {
                // 遇到右括号,弹出栈顶元素(匹配一个左括号)
                stack.pop();
                
                if (stack.isEmpty()) {
                    // 栈空说明当前右括号无匹配,作为新边界
                    stack.push(i);
                } else {
                    // 计算当前有效长度:i - 栈顶元素(即有效子串起始位置)
                    maxLen = Math.max(maxLen, i - stack.peek());
                }
            }
        }
        return maxLen;
    }
}

算法分析

  • 时间复杂度:O(n)
    • 两种方法都只需遍历字符串一次
  • 空间复杂度
    • 动态规划:O(n)
    • 栈方法:O(n)(最坏情况全为 '('

算法过程(栈方法)

s = ")()())"

  1. 初始化stack = [-1], maxLen=0
  2. i=0')' → 弹出 -1 → 栈空 → 压入 0stack=[0]
  3. i=1'(' → 压入 1stack=[0,1]
  4. i=2')' → 弹出 1 → 栈非空 → len=2-0=2maxLen=2
  5. i=3'(' → 压入 3stack=[0,3]
  6. i=4')' → 弹出 3 → 栈非空 → len=4-0=4maxLen=4
  7. i=5')' → 弹出 0 → 栈空 → 压入 5stack=[5]
  8. 结果maxLen=4

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 测试用例1:标准示例
    String s1 = ")()())";
    System.out.println("Test 1: " + solution.longestValidParentheses(s1)); // 4
    
    // 测试用例2:嵌套括号
    String s2 = "(()())";
    System.out.println("Test 2: " + solution.longestValidParentheses(s2)); // 6
    
    // 测试用例3:不连续有效括号
    String s3 = "()(()";
    System.out.println("Test 3: " + solution.longestValidParentheses(s3)); // 2
    
    // 测试用例4:全无效
    String s4 = "))))";
    System.out.println("Test 4: " + solution.longestValidParentheses(s4)); // 0
    
    // 测试用例5:空字符串
    String s5 = "";
    System.out.println("Test 5: " + solution.longestValidParentheses(s5)); // 0
    
    // 测试用例6:单对括号
    String s6 = "()";
    System.out.println("Test 6: " + solution.longestValidParentheses(s6)); // 2
    
    // 测试用例7:复杂嵌套
    String s7 = "()(())(";
    System.out.println("Test 7: " + solution.longestValidParentheses(s7)); // 6
}

关键点

  1. 动态规划核心逻辑

    • "()"dp[i] = dp[i-2] + 2
    • "))"dp[i] = dp[i-1] + 2 + dp[i - dp[i-1] - 2]
  2. 栈方法核心逻辑

    • 栈底始终保存最后一个未匹配右括号位置
    • 遇到 ')' 时:
      • 弹出后栈空 → 压入当前索引(新边界)
      • 栈非空 → 更新最大长度
  3. 边界处理

    • 动态规划:注意 i-2i-dp[i-1]-2 的索引有效性
    • 栈方法:初始压入 -1 处理边界计算

常见问题

  1. 为什么栈方法要压入 -1

    • 提供初始边界,使 i - stack.peek() 能正确计算从开头开始的子串长度
  2. 动态规划中如何处理 "))" 型?

    • 需要检查跨过内部有效子串前的字符是否为 '('
    • 累加内部有效长度(dp[i-1])和前面的有效长度(dp[i - dp[i-1] - 2]
  3. 哪种方法更优?

    • 栈方法更直观易实现,动态规划空间效率略高(但栈方法更常用)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值