LeetCode-678. 有效的括号字符串
题目来源
题目详情
给定一个只包含三种字符的字符串:(
,)
和 *
,写一个函数来检验这个字符串是否为有效字符串。有效字符串具有如下规则:
- 任何左括号
(
必须有相应的右括号)
。 - 任何右括号
)
必须有相应的左括号(
。 - 左括号
(
必须在对应的右括号之前)
。 *
可以被视为单个右括号)
,或单个左括号(
,或一个空字符串。- 一个空字符串也被视为有效字符串。
示例 1:
输入: "()"
输出: True
示例 2:
输入: "(*)"
输出: True
示例 3:
输入: "(*))"
输出: True
注意:
- 字符串大小将在 [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;
}
}
解法二:二维动态规划
- 本题一看题目就知道是一道动态规划相关的题目,而且这个题型与之前遇到的5-最长回文子串以及647-回文子串这两道题目很相似,因为它们都是一种需要考虑中间子串的情况。这是与32-最长有效括号这道题目最大的一个区别,因为LeetCode-32这道题只有左右括号两种字符,所以它可以使用一维的dp数组来表示最长有效括号的长度,但是本题除了左右括号还有一个特殊的'*'号,它可以充当任意一个符号,甚至空字符,这就导致了如果使用一维dp将无法根据前面的状态进行转移。
- 因此,这里我们借助回文子串的解题思路,定义一个
dp
数组为boolean[][] dp
,其中\(dp[i][j]\)表示(i,j)的子串是否是有效的括号字符串。而它们的状态转移如下:
\[dp[i][j] = dp[i+1][j-1] || (dp[i][k] \&\& dp[k+1][j])
\]
- 上面的状态转移方程涉及到两种情况:
- 第一种,如果i和j所处的字符是合法的括号对,即分别为
(
和)
字符,或者是使用了‘*’替换后的有效括号对,那么\(dp[i][j]\)的状态可以根据\(dp[i+1][j-1]\)来判断。 - 第二种,如果上述得出的\(dp[i][j]\)是false,那就要进入这种情况的判断了。我们需要遍历i到j之间的字符,看是否可以找到一个字符k,使得(i,k)和(k,j)都是有效的括号字符串,如果能找到说明\(dp[i][j]\)也是合法的括号字符串。
- 第一种,如果i和j所处的字符是合法的括号对,即分别为
- 本题需要注意的是,因为我们都是成对来考虑的,所以我们需要对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];
}
}
Either Excellent or Rusty