题目描述

image.png

题目链接

https://leetcode.cn/problems/longest-valid-parentheses/

思路

1. 动态规划

我们定义【32】最长有效括号 - 图2表示以下标【32】最长有效括号 - 图3字符结尾的最长有效括号的长度。我们将【32】最长有效括号 - 图4数组全部初始化为【32】最长有效括号 - 图5。显然有效的子串一定以【32】最长有效括号 - 图6结尾,因此我们可以知道以【32】最长有效括号 - 图7结尾的子串对应的【32】最长有效括号 - 图8值必定为【32】最长有效括号 - 图9,因为它肯定不是一个有效的子串,我们只需要求解【32】最长有效括号 - 图10【32】最长有效括号 - 图11数组中对应位置的值。

我们从前往后遍历字符串求解【32】最长有效括号 - 图12值,我们每两个字符检查一次:

  • 【32】最长有效括号 - 图13【32】最长有效括号 - 图14,也就是字符串形如【32】最长有效括号 - 图15,我们可以推出:

    【32】最长有效括号 - 图16

我们可以进行这样的转移,是因为结束部分的【32】最长有效括号 - 图17是一个有效的子字符串,并且将之前有效子字符串的长度增加了【32】最长有效括号 - 图18
image.png

  • 【32】最长有效括号 - 图20【32】最长有效括号 - 图21,也就是字符串形如【32】最长有效括号 - 图22,我们可以推出:

    如果【32】最长有效括号 - 图23,那么【32】最长有效括号 - 图24

image.png
我们考虑如果倒数第二个【32】最长有效括号 - 图26是一个有效子字符串的一部分(记作【32】最长有效括号 - 图27),对于最后一个【32】最长有效括号 - 图28,如果它是一个更长子字符串的一部分,那么它一定有一个对应的【32】最长有效括号 - 图29,且它的位置在倒数第二个【32】最长有效括号 - 图30所在的有效子字符串的前面(也就是【32】最长有效括号 - 图31的前面)。因此,如果子字符串【32】最长有效括号 - 图32的前面恰好是【32】最长有效括号 - 图33,那么我们就用【32】最长有效括号 - 图34加上【32】最长有效括号 - 图35的长度(【32】最长有效括号 - 图36)去更新【32】最长有效括号 - 图37

同时,我们也会把有效子串【32】最长有效括号 - 图38之前的有效子串的长度也加上,也就是再加上【32】最长有效括号 - 图39。因为可能会存在以下这种情况:
image.png
最后的答案即为【32】最长有效括号 - 图41数组中的最大值。具体代码实现如下:

  1. public int longestValidParentheses(String s) {
  2. int maxLen = 0;
  3. int[] dp = new int[s.length()];
  4. for (int i = 1; i < s.length(); i++) {
  5. if (s.charAt(i) == ')') {
  6. // 判断其前一个字符
  7. if (s.charAt(i - 1) == '(') {
  8. // ...()
  9. dp[i] = i >= 2 ? dp[i - 2] + 2 : 2;
  10. } else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') {
  11. // ((...))
  12. dp[i] = dp[i - 1] + (i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] + 2 : 2;
  13. }
  14. maxLen = Math.max(maxLen, dp[i]);
  15. }
  16. }
  17. return maxLen;
  18. }
  • 时间复杂度:【32】最长有效括号 - 图42,其中【32】最长有效括号 - 图43为字符串的长度。我们只需遍历整个字符串一次,即可将【32】最长有效括号 - 图44数组求出来。
  • 空间复杂度:【32】最长有效括号 - 图45。我们需要一个大小为【32】最长有效括号 - 图46【32】最长有效括号 - 图47数组。

    2. 栈

    撇开方法一提及的动态规划方法,相信大多数人对于这题的第一直觉是找到每个可能的子串后判断它的有效性,但这样的时间复杂度会达到【32】最长有效括号 - 图48,无法通过所有测试用例。我们知道栈在处理括号匹配有着天然的优势,因此通过栈,我们可以在遍历给定字符串的过程中去判断到目前为止扫描的子串的有效性,同时也能得到最长有效括号的长度。

由于我们计算的是有效括号的长度,因此栈中元素存储的是下标值。具体做法是我们始终保持栈底元素为当前已经遍历过的元素中「最后一个没有被匹配的右括号的下标」,这样的做法主要是考虑了边界条件的处理,栈里其他元素维护左括号的下标:

  • 对于遇到的每个【32】最长有效括号 - 图49,我们将它的下标放入栈中。
  • 对于遇到的每个【32】最长有效括号 - 图50,我们先弹出栈顶元素表示匹配了当前右括号:
    • 如果栈为空,说明当前的右括号为没有被匹配的右括号,我们将其下标放入栈中来更新「最后一个没有被匹配的右括号的下标」。
    • 如果栈不为空,当前右括号的下标减去栈顶元素即为「以该右括号为结尾的最长有效括号的长度」。

我们从前往后遍历字符串并更新答案即可。需要注意的是,如果一开始栈为空,第一个字符为左括号的时候我们会将其放入栈中,这样就不满足提及的「最后一个没有被匹配的右括号的下标」,为了保持统一,我们在一开始的时候往栈中放入一个值为【32】最长有效括号 - 图51的元素。 具体代码实现如下:

  1. public int longestValidParentheses2(String s) {
  2. int maxLength = 0;
  3. Stack<Integer> stack = new Stack<>();
  4. stack.push(-1);
  5. for (int i = 0; i < s.length(); i++) {
  6. // 遇到左括号入栈
  7. if (s.charAt(i) == '(') {
  8. stack.push(i);
  9. } else {
  10. // 遇到右括号先出栈
  11. stack.pop();
  12. if (stack.empty()) {
  13. // 记录最后一个没有被匹配的右括号的下标
  14. stack.push(i);
  15. } else {
  16. // 更新最长有效括号
  17. maxLength = Math.max(maxLength, i - stack.peek());
  18. }
  19. }
  20. }
  21. return maxLength;
  22. }
  • 时间复杂度:【32】最长有效括号 - 图52【32】最长有效括号 - 图53是给定字符串的长度。我们只需要遍历字符串一次即可。
  • 空间复杂度:【32】最长有效括号 - 图54。栈的大小在最坏情况下会达到【32】最长有效括号 - 图55,因此空间复杂度为【32】最长有效括号 - 图56

    3. 双向遍历

    实际上,我们也可以不使用栈来判断括号有效性。具体做法类似:【22】括号生成 里的实现,通过维护左括号和右括号的数量来判断括号是否有效。这样的话,空间复杂度就能降到【32】最长有效括号 - 图57

具体地,我们利用两个计数器【32】最长有效括号 - 图58【32】最长有效括号 - 图59。首先,我们从左到右遍历字符串,对于遇到的每个【32】最长有效括号 - 图60,我们增加【32】最长有效括号 - 图61计数器,对于遇到的每个【32】最长有效括号 - 图62,我们增加【32】最长有效括号 - 图63计数器。每当【32】最长有效括号 - 图64计数器与【32】最长有效括号 - 图65计数器相等时,我们计算当前有效字符串的长度,并且记录目前为止找到的最长子字符串。当【32】最长有效括号 - 图66计数器比【32】最长有效括号 - 图67计数器大时,我们将【32】最长有效括号 - 图68【32】最长有效括号 - 图69计数器同时变回【32】最长有效括号 - 图70

这样的做法贪心地考虑了以当前字符下标结尾的有效括号长度,每次当右括号数量多于左括号数量的时候之前的字符我们都扔掉不再考虑,重新从下一个字符开始计算,但这样会漏掉一种情况,就是遍历的时候左括号的数量始终大于右括号的数量,即【32】最长有效括号 - 图71 ,这种时候最长有效括号是求不出来的。解决的方法也很简单,我们只需要从右往左遍历用类似的方法计算即可,只是这个时候判断条件反了过来:

  • 【32】最长有效括号 - 图72【32】最长有效括号 - 图73计数器大时,我们将【32】最长有效括号 - 图74【32】最长有效括号 - 图75计数器同时变回 00。
  • 【32】最长有效括号 - 图76【32】最长有效括号 - 图77计数器相等时,计算当前有效字符串的长度,并记录目前为止找到的最长子字符串。

这样我们就能涵盖所有情况从而求解出答案。具体代码实现如下:

  1. public int longestValidParentheses(String s) {
  2. int left = 0, right = 0, maxLength = 0;
  3. // 从左到右遍历
  4. for (int i = 0; i < s.length(); i++) {
  5. if (s.charAt(i) == '(') {
  6. left++;
  7. } else {
  8. right++;
  9. }
  10. if (left == right) {
  11. // 更新有效括号的最大长度
  12. maxLength = Math.max(maxLength, 2 * right);
  13. } else if (right > left) {
  14. left = right = 0;
  15. }
  16. }
  17. // 从右到左遍历
  18. left = right = 0;
  19. for (int i = s.length() - 1; i >= 0; i--) {
  20. if (s.charAt(i) == '(') {
  21. left++;
  22. } else {
  23. right++;
  24. }
  25. if (left == right) {
  26. maxLength = Math.max(maxLength, 2 * left);
  27. } else if (left > right) {
  28. left = right = 0;
  29. }
  30. }
  31. return maxLength;
  32. }
  • 时间复杂度:【32】最长有效括号 - 图78,其中【32】最长有效括号 - 图79为字符串长度。我们只要正反遍历两边字符串即可。
  • 空间复杂度:【32】最长有效括号 - 图80。我们只需要常数空间存放若干变量。