数据结构与算法之深入解析“最长有效括号”的求解思路与算法示例

Posted Serendipity·y

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构与算法之深入解析“最长有效括号”的求解思路与算法示例相关的知识,希望对你有一定的参考价值。

一、题目要求

  • 给你一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
  • 示例 1:
输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"
  • 示例 2:
输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"
  • 示例 3:
输入:s = ""
输出:0
  • 提示:
    • 0 <= s.length <= 3 * 104;
    • s[i] 为 ‘(’ 或 ‘)’。

二、求解算法

① 动态规划

  • 结合题目,有“最长”要求,可以考虑尝试使用“动态规划”进行分析,这是一个“最值型”的动态规划题目。
  • 动态规划题目分析的 4 个步骤:
    • 确定状态:研究最优策略的最后一步,化为子问题;
    • 转移方程:根据子问题定义得到;
    • 初始条件和边界情况;
    • 计算顺序。
  • 首先,定义一个 dp 数组,其中第 i 个元素表示以下标为 i 的字符结尾的最长有效子字符串的长度。
  • 确定状态的最后一步,对于最优的策略,一定有最后一个元素 s[i],因此先看第 i 个位置,这个位置的元素 s[i] 可能有如下两种情况:
    • s[i]== ′(′,s[i] 无法和其之前的元素组成有效的括号对,所以 dp[i]=0;
    • s[i]== ′)′,需要看其前面对元素来判断是否有有效括号对。
  • s[i-1]== ′(′,即 s[i] 和 s[i−1] 组成一对有效括号,有效括号长度新增长度 2,i 位置对最长有效括号长度为其之前 2 个位置的最长括号长度加上当前位置新增的 2,无需知道i−2位置对字符是否可以组成有效括号对。那么有:dp[i]=dp[i−2]+2;

  • s[i−1] == ′)′,这种情况下,如果前面有和 s[i] 组成有效括号对的字符,即形如 ((…)),这样的话,就要求 s[i−1] 位置必然是有效的括号对,否则 s[i] 无法和前面对字符组成有效括号对。这时,只需要找到和 s[i] 配对对位置,并判断其是否是 ( 即可。和其配对对位置为:i−dp[i−1]−1。
  • 如果 s[i−dp[i−1]−1]== ′(′,有效括号长度新增长度 2,i 位置对最长有效括号长度为 i-1 位置的最长括号长度加上当前位置新增的 2,那么有:dp[i]=dp[i−1]+2。值得注意的是,i−dp[i−1]−1 和 i 组成了有效括号对,这将是一段独立的有效括号序列,如果之前的子序列是形如 (…) 这种序列,那么当前位置的最长有效括号长度还需要加上这一段,所以 dp[i]=dp[i−1]+dp[i−dp[i−1]−2]+2。

  • 子问题:根据上面的分析,可以得到如下两个计算公式:
    • dp[i]=dp[i−2]+2
    • dp[i]=dp[i−1]+dp[i−dp[i−1]−2]+2
  • 那么,求 dp[i] 就变成了求 dp[i−1]、 dp[i−2]、dp[i−dp[i−1]−2] 的子问题。这样状态也明确了:设 dp 数组,其中第 i 个元素表示以下标为 i 的字符结尾的最长有效子字符串的长度。
  • 转移方程:子问题明确后,转移方程直接由子问题得到:
if s[i] == '(' :
    dp[i] = 0
if s[i] == ')' :
    if s[i - 1] == '(' :
        dp[i] = dp[i - 2] + 2 #要保证i - 2 >= 0

    if s[i - 1] == ')' and s[i - dp[i - 1] - 1] == '(' :
        dp[i] = dp[i - 1] + dp[i - dp[i - 1] - 2] + 2 #要保证i - dp[i - 1] - 2 >= 0
  • 初始条件和边界情况:
    • 初始条件: dp[i]=0;
    • 边界情况:需要保证计算过程中:i−2>=0 和 i−dp[i−1]−2>=0。
  • 计算顺序:
    • 无论第一个字符是什么,都有:dp[0]=0;
    • 然后依次计算:dp[1],dp[2],…,dp[n−1];
    • 结果是: max(dp[i])。
  • C++ 示例:
class Solution 
public:
    int longestValidParentheses(string s) 
        int size = s.length();
        vector<int> dp(size, 0);

        int maxVal = 0;
        for(int i = 1; i < size; i++) 
            if (s[i] == ')') 
                if (s[i - 1] == '(') 
                    dp[i] = 2;
                    if (i - 2 >= 0) 
                        dp[i] = dp[i] + dp[i - 2];
                    
                 else if (dp[i - 1] > 0) 
                    if ((i - dp[i - 1] - 1) >= 0 && s[i - dp[i - 1] - 1] == '(') 
                        dp[i] = dp[i - 1] + 2;
                        if ((i - dp[i - 1] - 2) >= 0) 
                            dp[i] = dp[i] + dp[i - dp[i - 1] - 2];
                        
                    
                
            
            maxVal = max(maxVal, dp[i]);
        
        return maxVal;
    
;
  • Java 示例:
class Solution 
    public int longestValidParentheses(String s) 
        char[] chs = s.toCharArray();
        int[] dp = new int[chs.length];
        int res = 0;
        for (int i = 1; i < chs.length; i++) 
            if (chs[i] == ')') 
                if (chs[i - 1] == '(') dp[i] = 2 + ((i - 2 >= 0) ? dp[i - 2] : 0);
                else 
                    int x = i - dp[i-1] - 1 ;
                    boolean flag = x >= 0 && chs[x] == '(' && (dp[i] = dp[i-1] + 2 + (x >= 1 ? dp[x-1] : 0)) > 1;
                
                res = Math.max(res, dp[i]);
            
        
        return res;

    

② 栈

  • 撇开动态规划方法,相信大多数人对于这题的第一直觉是找到每个可能的子串后判断它的有效性,但这样的时间复杂度会达到 O(n3),无法通过所有测试用例。但是通过栈,可以在遍历给定字符串的过程中去判断到目前为止扫描的子串的有效性,同时能得到最长有效括号的长度。
  • 具体做法是始终保持栈底元素为当前已经遍历过的元素中「最后一个没有被匹配的右括号的下标」,这样的做法主要是考虑了边界条件的处理,栈里其他元素维护左括号的下标:
    • 对于遇到的每个 ‘(’ ,将它的下标放入栈中;
    • 对于遇到的每个 ‘)’ ,先弹出栈顶元素表示匹配了当前右括号:如果栈为空,说明当前的右括号为没有被匹配的右括号,将其下标放入栈中来更新我们之前提到的「最后一个没有被匹配的右括号的下标」;如果栈不为空,当前右括号的下标减去栈顶元素即为「以该右括号为结尾的最长有效括号的长度」。
  • 我们从前往后遍历字符串并更新答案即可。需要注意的是,如果一开始栈为空,第一个字符为左括号的时候将其放入栈中,这样就不满足提及的「最后一个没有被匹配的右括号的下标」。
  • 为了保持统一,在一开始的时候往栈中放入一个值为 −1 的元素:

  • 首先 0 入栈:

  • 0 出栈:

  • -1 出栈:

  • 此时,栈为空:

  • 继续,2 入栈:

  • 3 入栈:

  • 4 入栈:

  • 5 入栈:

  • 5 出栈:

  • 再 4 出栈:

  • Java 示例:
class Solution 
    public int longestValidParentheses(String s) 
        int maxans = 0;
        Deque<Integer> stack = new LinkedList<Integer>();
        stack.push(-1);
        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 
                    maxans = Math.max(maxans, i - stack.peek());
                
            
        
        return maxans;
    

  • C++ 示例:
class Solution 
public:
    int longestValidParentheses(string s) 
        int maxans = 0;
        stack<int> stk;
        stk.push(-1);
        for (int i = 0; i < s.length(); i++) 
            if (s[i] == '(') 
                stk.push(i);
             else 
                stk.pop();
                if (stk.empty()) 
                    stk.push(i);
                 else 
                    maxans = max(maxans, i - stk.top());
                
            
        
        return maxans;
    
;

以上是关于数据结构与算法之深入解析“最长有效括号”的求解思路与算法示例的主要内容,如果未能解决你的问题,请参考以下文章

数据结构与算法之深入解析“股票的最大利润”的求解思路与算法示例

数据结构与算法之深入解析“安装栅栏”的求解思路与算法示例

数据结构与算法之深入解析“最长连续序列”的求解思路与算法示例

数据结构与算法之深入解析“路径总和”的求解思路与算法示例

数据结构与算法之深入解析“斐波那契数”的求解思路与算法示例

数据结构与算法之深入解析“连续整数求和”的求解思路与算法示例