题目
给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
示例 1:
输入:s = "(()"
输出:2
解释:最长有效括号子串是 "()"
示例 2:输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"
示例 3:输入:s = ""
输出:0来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-valid-parentheses
思路
这是一个困难级别的题,一开始并没有好的思路,用了暴力法去解决这个问题。
暴力法就是穷举所有可能的子串,判断子串是否为有效子串。这里我用到了几个方法去进行优化:(1)把所有子串记录下来(2)长度从短到长进行穷举,并利用记录的子串情况进行判断(3)子串长度为奇数是不可能为有效子串的。代码如下:
class Solution {
public int longestValidParentheses(String s) {
if (s == null || s.length() < 2) return 0;
boolean [][]valid = new boolean[s.length()][s.length()];
int max = 0;
for (int length = 2; length <= s.length(); length += 2) {
for (int start = 0; start <= s.length() - length; start++) {
if ((length == 2 || valid[start+1][start+length-2]) && s.charAt(start) == '(' && s.charAt(start + length - 1) == ')') {
valid[start][start+length-1] = true;
max = length;
} else {
for (int i = start+1; i < start + length; i += 2) {
if (valid[start][i] && valid[i+1][start+length-1]) {
valid[start][start+length-1] = true;
max = length;
break;
}
}
}
}
}
return max;
}
}
时间复杂度:O(n^3),空间复杂度O(n^2)
上面的解法,没有充分利用有效括号的特点。其中一个很重要的特点就是:一个有效子串要么被最长子串包括,要么与最长子串无交集。所以,我们可以基于当前的有效子串进行扩展,从而找到那个最长有效子串。
想象一下,我们在一次遍历字符串的过程中,能找到一些匹配的子串,然后往下一步走的时候,这个子串被后面的子串去包含住,也就是(xxxx)的形式,要么是两个有效子串连起来了xxxxyyyy的形式。如果有多组有效子串,我们都记录下来,可以在往后面遍历的过程中去看新的有效子串能否与旧的有效子串连接起来。代码如下:
import java.util.Stack;
class Solution {
public int longestValidParentheses(String s) {
if (s == null || s.length() < 2) return 0;
Stack<Match> matches = new Stack<>(); // 记录已经出现且不相连的所有有效子串
for (int i = 1; i < s.length(); i++) { // 遍历字符串
Match lastMatch = matches.empty() ? null : matches.peek();
// 如果之前有匹配的,且下一个要看的字符就与上次匹配的子串相邻,看能否将旧的匹配子串包含起来
if (lastMatch != null && lastMatch.right == i - 1) {
if (lastMatch.left > 0) {
if (s.charAt(i) == ')' && s.charAt(lastMatch.left - 1) == '(') {
lastMatch.left -= 1;
lastMatch.right += 1;
}
}
} else { // 两种情况:1、还没有过匹配子串;2、在检验的字符与之前匹配的子串不相邻
if (s.charAt(i) == ')' && s.charAt(i - 1) == '(') {
Match currentMatch = new Match(i - 1, i);
matches.push(currentMatch);
}
}
// 检查一下最近两次匹配的子串是否可以连接起来
if (matches.size() >= 2) {
Match endMatch = matches.pop();
Match llastMatch = matches.pop();
if (endMatch.left == llastMatch.right + 1) {
endMatch.left = llastMatch.left;
matches.push(endMatch);
} else {
matches.push(llastMatch);
matches.push(endMatch);
}
}
}
// 取出所有匹配中的最大值
int max = 0;
while (!matches.empty()) {
Match match = matches.pop();
if (match != null) {
max = Math.max(max, match.right - match.left + 1);
}
}
return max;
}
class Match {
int left;
int right;
Match(int left, int right) {
this.left = left;
this.right = right;
}
}
}
时间复杂度:O(n),空间复杂度:O(1)
但这个题最好的解题思路是动态规划。因为用动态规划,思路更理论化。但是很难去想到动态规划的状态转移方程。因为很多时候,这种本来求所有可能性的问题,都需要固定住一个条件才能更好的去理解动态规划。比如这个题,本来是要求所有子串的可能性,就需要把题目转化成以某位字符结尾的子串。dp[i]为以i结尾的子串中最长子串的长度,此时有了i,有了最长长度,我们也就能知道这个最长子串的起始位置为i-dp[i]+1
我们上面说过,针对已有的有效子串,扩展的方式有两种:(1)被包含起来,也就是(xxxx)(2)与其他有效子串相连,也就是xxxxyyyy
那么dp[i]可以是与dp[i-1]这个最长子串前的左括号联合把dp[i-1]包含起来,这里其实还有一个复杂点,就是当新生成一个有效子串之后,还需要再往左括号之前看是否能连接成更长的有效子串,也就是yyyy(xxxx);另一种形式是与dp[i-2]通过一个匹配括号()连接起来
然后,我们有了以字符串每一位结尾的有效子串长度,取其中最大的一个即可。当然在实现过程中是可以以一个变量的形式记录的,但是思考过程中其实是又一个O(n)的复杂度,所以有时候是会碍于这个复杂度而放弃这一层思考。。。
代码如下:
public class Solution {
public int longestValidParentheses(String s) {
int maxans = 0;
int[] dp = new int[s.length()];
for (int i = 1; i < s.length(); i++) {
if (s.charAt(i) == ')') {
if (s.charAt(i - 1) == '(') {
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
} else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') {
dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
}
maxans = Math.max(maxans, dp[i]);
}
}
return maxans;
}
}
耗时:130分钟,动态规划的题需要多练习