leetcode:301. 删除无效的括号

题目

给你一个由若干括号和字母组成的字符串 s ,删除最小数量的无效括号,使得输入的字符串有效。
返回所有可能的结果。答案可以按 任意顺序 返回。

示例:

  1. 输入:s = "()())()"
  2. 输出:["(())()","()()()"]
  1. 输入:s = "(a)())()"
  2. 输出:["(a())()","(a)()()"]
  1. 输入:s = ")("
  2. 输出:[""]

解答 & 代码

解法一:递归回溯 + 剪枝

  1. class Solution {
  2. private:
  3. string originS; // 原字符串 s
  4. int strLen; // 原字符串 s 的长度
  5. int resultLen; // 删除最小数量的无效括号后,剩余字符串的长度
  6. // maxScore 用于剪枝,即最终答案字符串中括号对数的最大值,若递归过程中 score 超出 [0, maxScore] 则剪枝
  7. int maxScore; // 搜索过程中可能出现的最大得分(= 原字符串 s 中左括号数量、右括号数量的最小值)
  8. unordered_set<string> result; // 用哈希表存储所有可能的结果,以达到去重的目的
  9. /* 递归回溯搜索 + 剪枝
  10. * param idx: 当前遍历到原字符串 s 的下标
  11. * param curS: 当前得到的结果字符串(遍历 s[idx] 之前形成的结果字符串)
  12. * param leftDeleteNum: 当前剩余应删除的左括号的数目
  13. * param rightDeleteNum: 当前剩余应删除的右括号的数目
  14. * param score: 当前的得分。若结果字符串 curS 保留一个左括号,则 score + 1;
  15. 若结果字符串 curS 保留一个右括号,则 score - 1
  16. */
  17. void dfs(int idx, string curS, int leftDeleteNum, int rightDeleteNum, int score)
  18. {
  19. // 递归结束条件 1:如果剩余应删除的左括号数目 or 右括号数目 < 0
  20. // 或者 score 超出范围 [0, maxScore] 则剪枝:
  21. // - score < 0 说明 curS 中右括号多于左括号;
  22. // - score > maxScore 说明保留的左括号数超过了应保留的括号对数的最大数目
  23. if(leftDeleteNum < 0 || rightDeleteNum < 0 || score < 0 || score > maxScore)
  24. return;
  25. // 如果剩余应删除的左括号数目 and 右括号数目都为 0
  26. // 若当前结果字符串 curS 的长度 等于 删除最小数量的无效括号后剩余字符串的长度
  27. // 则,将当前结果字符串 curS 插入哈希表
  28. if(leftDeleteNum == 0 && rightDeleteNum == 0)
  29. if(curS.size() == resultLen)
  30. result.insert(curS);
  31. // 递归结束条件 2:如果当前已遍历完字符串 s,则结束递归
  32. if(idx == strLen)
  33. return;
  34. // 当前遍历到的字符串
  35. char ch = originS[idx];
  36. if(ch == '(')
  37. {
  38. // 保留当前的左括号,则剩余应删的左括号数目不变,score + 1
  39. dfs(idx + 1, curS + ch, leftDeleteNum, rightDeleteNum, score + 1);
  40. // 删除当前的左括号,则剩余应删的左括号数目 - 1,score 不变
  41. dfs(idx + 1, curS, leftDeleteNum - 1, rightDeleteNum, score);
  42. }
  43. else if(ch == ')')
  44. {
  45. // 保留当前的右括号,则剩余应删的右括号数目不变,score - 1
  46. dfs(idx + 1, curS + ch, leftDeleteNum, rightDeleteNum, score - 1);
  47. // 删除当前的右括号,则剩余应删的右括号数目 - 1,score 不变
  48. dfs(idx + 1, curS, leftDeleteNum, rightDeleteNum - 1, score);
  49. }
  50. else // 当前字符不是括号,则保留
  51. dfs(idx + 1, curS + ch , leftDeleteNum, rightDeleteNum, score);
  52. }
  53. public:
  54. vector<string> removeInvalidParentheses(string s) {
  55. originS = s; // 原字符串 s
  56. strLen = s.size(); // 原字符串 s 的长度
  57. int leftDeleteNum = 0; // 最少应该删除的左括号数量
  58. int rightDeleteNum = 0; // 最少应该删除的右括号数量
  59. int leftBracketNum = 0; // 原字符串 s 中的左括号数
  60. int rightBracketNum = 0; // 原字符串 s 中的右括号数
  61. // 遍历字符串 s,统计得到以上 4 个变量的值
  62. for(int i = 0; i < s.size(); ++i)
  63. {
  64. if(s[i] == '(')
  65. {
  66. ++leftDeleteNum;
  67. ++leftBracketNum;
  68. }
  69. else if(s[i] == ')')
  70. {
  71. if(leftDeleteNum != 0)
  72. --leftDeleteNum;
  73. else
  74. ++rightDeleteNum;
  75. ++rightBracketNum;
  76. }
  77. }
  78. // 最后返回的结果字符串的长度,即 删除最小数量的无效括号后 剩余字符串的长度
  79. resultLen = s.size() - leftDeleteNum - rightDeleteNum;
  80. // 搜索过程中可能出现的合法的最大得分,
  81. // = 原字符串 s 中左括号数量、右括号数量的最小值,即最终答案字符串中括号对数的最大值
  82. // 最大得分为:合法的左括号先全部出现在左边,此时 score 达到最大
  83. // 如过 score 超过 maxScore,说明保留的左括号数超出上限了,剪枝
  84. // 因此,maxScore 其实是 score 的上界,用于剪枝
  85. maxScore = min(leftBracketNum, rightBracketNum);
  86. // 递归回溯
  87. dfs(0, "", leftDeleteNum, rightDeleteNum, 0);
  88. // 将哈希表中的结果存储到数组中,并返回
  89. vector<string> resultList;
  90. for(auto it = result.begin(); it != result.end(); ++it)
  91. resultList.push_back(*it);
  92. return resultList;
  93. }
  94. };

复杂度分析:设字符串 s 长为 N

  • 时间复杂度 [困难] 301. 删除无效的括号 - 图1
    • 预处理:计算 resultLenmaxScore 的时间复杂度 O(N)
    • 每个位置都有两个选择,保留字符 or 删除字符,搜索所有方案的时间复杂度 [困难] 301. 删除无效的括号 - 图2 (即每个字符串都有 [困难] 301. 删除无效的括号 - 图3 个子序列)。同时,搜索过程中会产生新的字符串,长度最大为 O(N)。因此整体时间复杂度 [困难] 301. 删除无效的括号 - 图4
  • 空间复杂度 [困难] 301. 删除无效的括号 - 图5:递归栈深度 = 字符串长度,且每次递归调用都要复制一次字符串,因此空间复杂度 [困难] 301. 删除无效的括号 - 图6

执行结果:

  1. 执行结果:通过
  2. 执行用时:8 ms, 在所有 C++ 提交中击败了 58.68% 的用户
  3. 内存消耗:13 MB, 在所有 C++ 提交中击败了 41.05% 的用户

解法二:BFS 广度优先搜索

题目中要求最少删除,这样的描述正是广度优先搜索算法应用的场景,并且题目也要求我们输出所有的结果。我们在进行广度优先搜索时每一轮删除字符串中的 1 个括号直到出现合法匹配的字符串为止此时进行轮转的次数即为最少需要删除括号的个数

  1. class Solution {
  2. private:
  3. bool isValid(string str)
  4. {
  5. int score = 0;
  6. for(int i = 0; i < str.size(); ++i)
  7. {
  8. if(str[i] == '(')
  9. ++score;
  10. if(str[i] == ')')
  11. {
  12. --score;
  13. if(score < 0)
  14. return false;
  15. }
  16. }
  17. return score == 0;
  18. }
  19. public:
  20. vector<string> removeInvalidParentheses(string s) {
  21. vector<string> result; // 结果数组
  22. unordered_set<string> curStrsMap; // 上一轮的搜索结果
  23. curStrsMap.insert(s); // 初始时插入 s
  24. // BFS 广度优先搜索
  25. while(true)
  26. {
  27. // 1. 遍历上一轮的搜索结果,逐个判断是否存在合法的结果,如果存在就插入到结果数组中
  28. // 判断字符串合法是看括号对是否匹配
  29. for(auto it = curStrsMap.begin(); it != curStrsMap.end(); ++it)
  30. {
  31. if(isValid(*it))
  32. result.push_back(*it);
  33. }
  34. // 如果已经出现了合法结果,则当前结果就是删除最少的无效括号后的有效字符串,就是答案
  35. if(result.size() > 0)
  36. return result; // 直接返回
  37. unordered_set<string> nextStrsMap; // 记录这一轮的搜索结果
  38. // 2. 遍历上一轮的搜索结果,尝试删除任意位置上的一个括号,删除后的结果都保存在 nextStrMap
  39. for(auto it = curStrsMap.begin(); it != curStrsMap.end(); ++it)
  40. {
  41. string curStr = *it;
  42. for(int i = 0; i < curStr.size(); ++i)
  43. {
  44. char ch = curStr[i];
  45. if(ch == '(' || ch == ')')
  46. nextStrsMap.insert(curStr.substr(0, i) + curStr.substr(i + 1, curStr.size() - i - 1));
  47. }
  48. }
  49. curStrsMap = nextStrsMap;
  50. }
  51. }
  52. };

复杂度分析:设字符串 s 长为 N

  • 时间复杂度 [困难] 301. 删除无效的括号 - 图7
  • 空间复杂度 [困难] 301. 删除无效的括号 - 图8:在进行第 i 轮迭代时,会从原始字符串中删除 i 个括号,因此第 i 轮迭代产生的字符串最多有 [困难] 301. 删除无效的括号 - 图9个,当 i = n/2 时组合数最大,此时迭代生成的字符串个数最多,因此空间复杂度为 [困难] 301. 删除无效的括号 - 图10

执行结果:

  1. 执行结果:通过
  2. 执行用时:60 ms, 在所有 C++ 提交中击败了 36.33% 的用户
  3. 内存消耗:14.9 MB, 在所有 C++ 提交中击败了 29.49% 的用户