这个题真的是一道好题,看起来如此简单,实际却如此复杂(应该说包含了很多知识)。
解法一:dp
此题的dp定义是比较大胆的(其实也挺常规的),就是以当前字符结尾的最长有效长度。说他大胆是因为这样定义的话,所有以左括号结尾的dp值都为0.
状态转移:如果当前字符是右括号,并且上一个是左括号,那么比较简单,
d
p
[
i
]
=
d
p
[
i
−
2
]
dp[i] = dp[i-2]
dp[i]=dp[i−2]
接下来就是核心:现在只剩一种情况,就是当前字符是右括号,上一个也是右括号。此时咱们看
d
p
[
i
−
1
]
dp[i-1]
dp[i−1]这个值,代表上一个的最佳长度。那么
s
[
i
−
d
p
[
i
−
1
]
−
1
]
s[i - dp[i-1] -1]
s[i−dp[i−1]−1]即为上一个最长串之前的字符,如果这个字符是左括号,那就匹配上了。这样右边的两个右括号就全部匹配完成:
d
p
[
i
]
=
d
p
[
i
−
1
]
+
d
p
[
i
−
d
p
[
i
−
1
]
−
2
]
+
2
dp[i] = dp[i-1]+dp[i-dp[i-1]-2]+2
dp[i]=dp[i−1]+dp[i−dp[i−1]−2]+2
class Solution {
public:
int longestValidParentheses(string s) {
int n = s.size();
vector<int> dp(n);
int ans = 0;
for(int i=1;i<n;i++)
{
if(s[i] == '(')continue;
if(s[i-1] == '(')
{
if(i-2<0)dp[i] = 2;
else dp[i] = dp[i-2]+2;
}
else if(i-1-dp[i-1] >= 0 && s[i-1-dp[i-1]] == '(')
{
if(i-2-dp[i-1]<0)dp[i] = dp[i-1]+2;
else dp[i] = dp[i-1] + dp[i-2-dp[i-1]] + 2;
}
if(dp[i] > ans)ans = dp[i];
}
return ans;
}
};
解法二:栈
先说下算法,真的很难想。
首先初始化栈里有个-1
如果遇见左括号,下标入栈。
如果遇见右括号,直接排出一个元素,然后看栈是否为空。
若空,放入当前下标,否则,当前下标减去栈顶元素更新答案。
算法解释:
这算法看起来真的是太简单了,甚至完全不知道它在干什么。
首先,我们要明确栈中元素的定义:上一个终止符位置,即遇到这个位置的字符,那肯定就不行了。对应的其实是右括号比左括号多的情况,如果右括号多了一个,那肯定没戏了。
所以,这套算法的执行逻辑是,栈顶记录上一个终止符,如果行就持续走,保持更新终止符。匹配也是一样的,左符号多时,左符号视为终止符,就这样持续记录(真变态的算法)
class Solution {
public:
int longestValidParentheses(string s) {
vector<int> v{-1};
int ans = 0;
for(int i=0;i<s.size();i++)
{
if(s[i] == '(')v.push_back(i);
else
{
v.pop_back();
if(v.size() == 0)v.push_back(i);
else ans = max(ans, i-v.back());
}
}
return ans;
}
};
解法三:两次遍历
这个也是老朋友了,和其他两次遍历能解决的问题一样,括号问题在两次遍历中一定有一次能保证取到最优值。
算法:记录左右括号的差值,右括号多时重新开始记录,差值为0时更新答案,两次遍历解决。
这个其实是不太好想的,单次遍历的局限性在于,如果左括号多了,那这部门的答案是得不到的。两次遍历能保证这种情况不会出现。因为对每一个最长串,他两边要么是左括号多,要么是右括号多,总有一次能找到它。
对于这类题目,其实都是有局部限制,即满足一定条件时,此时的值要重新计算:如糖果问题:你比左边小就设置为1,最大乘积子数组:遇见0重新开始算。
归根结底,还是两次遍历一定能让任意一个元素获得最优解。在实际问题中,我们要去分析能不能这样(可能还需要证明)。比如最大乘积子数组,是考虑负元素奇偶性后才能证明的。局部限制问题只能说是一个特例。