LeetCode-678. 有效的括号字符串

题目来源

678. 有效的括号字符串

题目详情

给定一个只包含三种字符的字符串:(  和 *,写一个函数来检验这个字符串是否为有效字符串。有效字符串具有如下规则:

  1. 任何左括号 ( 必须有相应的右括号 )
  2. 任何右括号 ) 必须有相应的左括号 ( 。
  3. 左括号 ( 必须在对应的右括号之前 )
  4. * 可以被视为单个右括号 ) ,或单个左括号 ( ,或一个空字符串。
  5. 一个空字符串也被视为有效字符串。

示例 1:

输入: "()"
输出: True

示例 2:

输入: "(*)"
输出: True

示例 3:

输入: "(*))"
输出: True

注意:

  1. 字符串大小将在 [1,100] 范围内。

相似题目

题号 题目 备注
20 有效的括号
22 括号生成 dfs
5 最长回文子串 dp
647 回文子串 dp
32 最长有效括号 dp
678 有效的括号字符串 栈、dp

题解分析

解法一:双栈

本题与 有效的括号 本题十分相似,题目都是求解最大括号匹配,但是这题特殊的是,这里除了左右括号还包括星号,而且该星号可以作为左右括号或者空进行使用。

考虑到星号的特殊性,这里还是使用栈来进行括号的匹配,只不过在匹配的时候需要将星号考虑在内,并且设置两个栈,分别用于存储左括号以及星号的出现序号(序号主要用来在最后判断左括号和星号的相对位置)。

  • 遇到左括号和星号都分别进栈
  • 遇到右括号时,优先考虑左括号栈:
    • 左括号栈不为空,则出左括号栈
    • 左括号栈为空,则如果星号栈不为空,出星号栈
    • 星号栈为空,则返回false
  • 在最后,左括号栈和星号栈可能不为空,这个时候还需要再次进行匹配,因为星号可以当右括号使用。需要注意的是,这里在匹配时需要考虑左括号和星号的相对位置,需要保证星号始终在左括号右边,否则匹配失败。
class Solution {
    public boolean checkValidString(String s) {
        int n = s.length();
        LinkedList<Integer> leftSta = new LinkedList<>();
        LinkedList<Integer> starSta = new LinkedList<>();
        for (int i=0; i<n; i++) {
            char ch = s.charAt(i);
            if (ch == '(') {
                leftSta.push(i);
            } else if (ch == '*') {
                starSta.push(i);
            } else {
                if (!leftSta.isEmpty()) {
                    leftSta.pop();
                } else if (!starSta.isEmpty()) {
                    starSta.pop();
                } else {
                    return false;
                }
            }
        }
        while (!leftSta.isEmpty()) {
            if (starSta.isEmpty()) {
                return false;
            }
            // 星号的位置小于左括号的位置
            if (starSta.pop() < leftSta.pop()) {
                return false;
            }
        }
        return true;
    }
}

解法二:二维动态规划

  1. 本题一看题目就知道是一道动态规划相关的题目,而且这个题型与之前遇到的5-最长回文子串以及647-回文子串这两道题目很相似,因为它们都是一种需要考虑中间子串的情况。这是与32-最长有效括号这道题目最大的一个区别,因为LeetCode-32这道题只有左右括号两种字符,所以它可以使用一维的dp数组来表示最长有效括号的长度,但是本题除了左右括号还有一个特殊的'*'号,它可以充当任意一个符号,甚至空字符,这就导致了如果使用一维dp将无法根据前面的状态进行转移。
  2. 因此,这里我们借助回文子串的解题思路,定义一个dp数组为boolean[][] dp,其中\(dp[i][j]\)表示(i,j)的子串是否是有效的括号字符串。而它们的状态转移如下:

\[dp[i][j] = dp[i+1][j-1] || (dp[i][k] \&\& dp[k+1][j]) \]

  1. 上面的状态转移方程涉及到两种情况:
    • 第一种,如果i和j所处的字符是合法的括号对,即分别为()字符,或者是使用了‘*’替换后的有效括号对,那么\(dp[i][j]\)的状态可以根据\(dp[i+1][j-1]\)来判断。
    • 第二种,如果上述得出的\(dp[i][j]\)是false,那就要进入这种情况的判断了。我们需要遍历i到j之间的字符,看是否可以找到一个字符k,使得(i,k)和(k,j)都是有效的括号字符串,如果能找到说明\(dp[i][j]\)也是合法的括号字符串。
  2. 本题需要注意的是,因为我们都是成对来考虑的,所以我们需要对dp数组进行一些特殊的初始化,比如需要分别初始化字符串长度为1和2时候的dp状态,这些状态的判断比较简单,这里就不详细说明了。
class Solution {
    public boolean checkValidString(String s) {
        int n = s.length();
        // dp[i][j]表示以(i,j)的子串是否是有效的括号字符串
        // dp[i][j] = dp[i+1][j-1] || (dp[i][k] && dp[k+1][j])
        boolean[][] dp = new boolean[n][n];
        for(int i=0; i<n; i++){
            if(s.charAt(i) == '*'){// '*'可以被视为一个空字符串,所以是合法的
                dp[i][i] = true;
            }
        }

        for(int i=1; i<n; i++){
            char ch1 = s.charAt(i-1);
            char ch2 = s.charAt(i);
            if((ch1 == '(' || ch1 == '*') && (ch2 == ')' || ch2 == '*')){
                // 长度为2的子串,且满足匹配规则则是合法的
                dp[i-1][i] = true;
            }
        }

        for(int i=n-3; i>=0; i--){
            char ch1 = s.charAt(i);
            for(int j=i + 2; j<n; j++){
                char ch2 = s.charAt(j);

                if((ch1 == '(' || ch1 == '*') && (ch2 == ')' || ch2 == '*')){
                    // 子串的格式为:(*****)
                    dp[i][j] = dp[i+1][j-1];
                }

                if(!dp[i][j]){
                    for(int k=i; k<j; k++){
                        // 子串格式为:()*****()
                        dp[i][j] = dp[i][k] && dp[k+1][j];
                        if(dp[i][j]){
                            break;
                        }
                    }
                }
            }
        }

        return dp[0][n-1];
    }
}
posted @ 2022-04-01 20:35  Garrett_Wale  阅读(547)  评论(0编辑  收藏  举报