算法细节系列(15):Valid Parentheses系列

算法细节系列(15):Valid Parentheses系列

详细代码可以fork下Github上leetcode项目,不定期更新。

题目均摘自leetcode:
1. Leetcode 020: Valid Parentheses
2. Leetcode 022: Great Parentheses
3. Leetcode 241: Different Ways to Add Parentheses
4. Leetcode 301: Remove Invalid Parentheses
5. Leetcode 032: Longest Valid Parentheses

Leetcode 020: Valid Parentheses

Problem:

Given a string containing just the characters ‘(‘, ‘)’, ‘{‘, ‘}’, ‘[’ and ‘]’, determine if the input string is valid.

The brackets must close in the correct order, “()” and “()[]{}” are all valid but “(]” and “([)]” are not.

思路:
括号的合法性总共就两个最基本的关系,合并关系()(),包容关系(()),其他情况都是这两种关系的组合。所以假设() == true,那么由于合并和包容的性质也为true,所以我们可以简单的认为() == "" == true,该问题就被解决了。

代码如下:

public boolean isValid(String s) {
        Stack<Character> stack = new Stack<Character>();
        for (int i = 0; i < s.length(); i++){
            if (s.charAt(i) == '[' || s.charAt(i) == '{' || s.charAt(i) == '('){
                stack.push(s.charAt(i));
            }
            else{
                char c = !stack.isEmpty() ? stack.peek() : '.';
                if (s.charAt(i) == ']' && c == '[') stack.pop();
                else if (s.charAt(i) == '}' && c == '{') stack.pop();
                else if (s.charAt(i) == ')' && c == '(') stack.pop();
                else return false;
            }
        }
        return stack.isEmpty();
    }

Leetcode 022: Great Parentheses

Problem:

Given n pairs of parentheses, write a function to generate all combinations of well-formed parentheses.

For example, given n = 3, a solution set is:

[
“((()))”,
“(()())”,
“(())()”,
“()(())”,
“()()()”
]

朴素的做法,考虑所有可能的情况,从中选择合法的序列。代码如下:

public List<String> generateParenthesis(int n) {
        List<String> ans = new ArrayList<>();
        helper(n, "", ans);
        return ans;
    }

    private void helper(int len, String tmp, List<String> ans){
        if (tmp.length() == 2 * len){
            if (isValid(tmp))
                ans.add(tmp);
            return;
        }

        tmp += '(';
        helper(len, tmp, ans);
        //回到递归前状态继续搜索
        tmp = tmp.substring(0, tmp.length()-1);
        tmp += ')';
        helper(len, tmp, ans);
        tmp = tmp.substring(0, tmp.length()-1);
    }

    public boolean isValid(String s) {
        Stack<Character> stack = new Stack<Character>();
        for (int i = 0; i < s.length(); i++){
            if (s.charAt(i) == '[' || s.charAt(i) == '{' || s.charAt(i) == '('){
                stack.push(s.charAt(i));
            }
            else{
                char c = !stack.isEmpty() ? stack.peek() : '.';
                if (s.charAt(i) == ']' && c == '[') stack.pop();
                else if (s.charAt(i) == '}' && c == '{') stack.pop();
                else if (s.charAt(i) == ')' && c == '(') stack.pop();
                else return false;
            }
        }
        return stack.isEmpty();
    }

上部分代码还有一个问题,tmp在递归函数外累加的一个坏处就是需要对其状态进行维护,而如果直接传参给递归函数的话,会随着函数的返回直接还原到先前状态,所以如上代码片段:

tmp += '(';
helper(len, tmp, ans);
tmp = tmp.substring(0, tmp.length()-1);
tmp += ')';
helper(len, tmp, ans);
tmp = tmp.substring(0, tmp.length()-1);

可以直接优化成:

helper(len, tmp + '(', ans);
helper(len, tmp + ')', ans);

代码可优化的地方很明显,没有剪去非合法的分枝,导致了很多没必要的搜索。所以,优化版本加入剪枝条件。

思路:
很简单,在生成过程中,只要有一个约束条件即能让序列合法。

  • 让添加的右括号的个数始终不能超过左括号的个数。(合法序列:左括号个数等于右括号个数)

所以,我们只要确保最终状态左括号数和右括号数相等,且在生成过程中始终保持左括号个数大于右括号数(注意:生成中的序列不能让左括号个数和右括号个数相等)。此时考虑终止条件,左括号个数等于题目给定的n即可。

代码如下:

public List<String> generateParenthesis(int n) {
        List<String> list = new ArrayList<String>();
        backtrack(list, "", 0, 0, n);
        return list;
    }

    public void backtrack(List<String> list, String str, int open, int close, int max){

        if(str.length() == max*2){
            list.add(str);
            return;
        }

        //终止条件(有限层数的搜索)
        if(open < max)
            backtrack(list, str+"(", open+1, close, max);
        //约束条件(序列合法性)
        if(close < open)
            backtrack(list, str+")", open, close+1, max);
    }

Leetcode 241: Different Ways to Add Parentheses

Problem:

Given a string of numbers and operators, return all possible results from computing all the different possible ways to group numbers and operators. The valid operators are +, - and *.

Example 1:

Input: “2-1-1”.

((2-1)-1) = 0
(2-(1-1)) = 2
Output: [0, 2]

Example 2:

Input: “2*3-4*5”

(2*(3-(4*5))) = -34
((2*3)-(4*5)) = -14
((2*(3-4))*5) = -10
(2*((3-4)*5)) = -10
(((2*3)-4)*5) = 10
Output: [-34, -14, -10, -10, 10]

思路:
都可以由每个符号划分成左右两部分,划分的两部分优先级最高,先进行计算。所以该问题就变成了一个递归问题。代码如下:

public List<Integer> diffWaysToCompute(String input) {
        List<Integer> ans = new ArrayList<>();
        for (int i = 0; i < input.length(); i++) {
            if (input.charAt(i) == '*' || input.charAt(i) == '-' || input.charAt(i) == '+') {
                List<Integer> left = diffWaysToCompute(input.substring(0, i));
                List<Integer> right = diffWaysToCompute(input.substring(i + 1));

                switch (input.charAt(i)) {
                case '*': {
                    for (int a1 : left) {
                        for (int a2 : right) {
                            ans.add(a1 * a2);
                        }
                    }
                }
                    break;
                case '-': {
                    for (int a1 : left) {
                        for (int a2 : right) {
                            ans.add(a1 - a2);
                        }
                    }
                }
                    break;

                case '+': {
                    for (int a1 : left) {
                        for (int a2 : right) {
                            ans.add(a1 + a2);
                        }
                    }
                }
                    break;
                default:
                    break;
                }
            }
        }
        //最基础的情况,由纯数字构成,直接返回即可。
        if (ans.size() == 0){
            ans.add(Integer.valueOf(input));
        }

        return ans;
    }

Leetcode 301: Remove Invalid Parentheses

Problem:

Remove the minimum number of invalid parentheses in order to make the input string valid. Return all possible results.

Note: The input string may contain letters other than the parentheses ( and ).

Examples:

“()())()” -> [“()()()”, “(())()”]
“(a)())()” -> [“(a)()()”, “(a())()”]
“)(” -> [“”]

思路:
我们知道一个合法的括号序列,左右括号个数相等。所以想法就是检查第一个出现非法状态的右括号,也就是说右括号的个数大于左括号的个数,那么怎样算合法呢?只要删除之前的任意一个右括号即可。一旦删除任意一个右括号,那么左半部分就合法了,而我们只需要递归去删除右半部分即可。为了完成该操作,需要记录两个指针,第一次非法出现的指针last_i以及删除右括号的位置last_j,避免出现重复的答案。

但如果遇到了类似((((),这种情况,我们知道,如果检测右括号那是检测不出非法状态的,所以我们可以把字符串逆序,重新使用一遍该算法,然后再把结果逆序回来,非常巧妙。代码如下:

    public List<String> removeInvalidParentheses(String s) {
        List<String> ans = new ArrayList<>();
        remove(s, ans, 0, 0, new char[]{'(',')'});
        return ans;
    }

    // last_i 记录了第一个非法的位置
    private void remove(String s, List<String> ans, int last_i, int last_j, char[] par){
        for (int stack = 0, i = last_i; i < s.length(); ++i){
            if (s.charAt(i) == par[0]) stack++;
            if (s.charAt(i) == par[1]) stack--;
            if (stack >= 0) continue;
            for (int j = last_j; j <= i; ++j){
                if (s.charAt(j) == par[1] && (j == last_j || s.charAt(j-1) != par[1])){ //去重
                    remove(s.substring(0, j) + s.substring(j+1, s.length()), ans, i, j, par);
                }
            }
            return;
        }
        String reversed = new StringBuilder(s).reverse().toString();
        if (par[0] == '('){
             remove(reversed, ans, 0, 0, new char[]{')', '('});
        }else{
            ans.add(reversed);
        }
    }

Leetcode 032: Longest Valid Parentheses

Problem:

Given a string containing just the characters ‘(’ and ‘)’, find the length of the longest valid (well-formed) parentheses substring.

For “(()”, the longest valid parentheses substring is “()”, which has length = 2.

Another example is “)()())”, where the longest valid parentheses substring is “()()”, which has length = 4.

朴素的做法:
使用栈,想法很简单,栈记录那些非法括号的下标,那么遍历一遍后,所有的合法括号都相互抵消,剩下的都是些无法抵消的非法位置。而非法位置之间的长度即为我们的候选答案,取所有候选最大即可。代码如下:

public int longestValidParentheses(String s) {
        int n = s.length();
        Stack<Integer> stack = new Stack<>();
        char[] c = s.toCharArray();
        for (int i = 0; i < n; i++){
            if (c[i] == '('){
                stack.push(i);
            }else{
                if (!stack.isEmpty()){
                    if (c[stack.peek()] == '(') stack.pop();
                    else stack.push(i);
                }else{
                    stack.push(i); //推入非法的下标
                }
            }
        }

        int max = 0;
        if (stack.isEmpty()) max = n; 
        else{
            int curr = n;
            int next = 0;
            while (!stack.isEmpty()){
                next = stack.pop();
                max = Math.max(max, curr-next-1);
                curr = next;
            }
            max = Math.max(max, curr);
        }
        return max;
    }

上述代码虽然运行时间为 O(n) ,但是为了抵消一些合法序列,需要进栈出栈两个操作,且最后还需要遍历一遍非法序列。

优化思路:
前文说了,合法序列有两种基本模式:合并关系和包容关系,如下。

合并关系:
()
包容关系:
(())

所以,对我们来说,只要我们能找到一种算法能够算出这两种关系的答案即可。
我们用dp表示:
dp[i]:表示在下标i的合法序列长度

如:
()   dp[1] = 2;
(()) dp[3] = 4;
())  dp[2] = 0;

我们知道合法序列的等价条件:左括号个数等于右括号个数
所以,我们以 ')'为目标,遇到 '('的情况,dp均为0,因为它不可能合法。

两种情况:
a.合并关系的更新:
()
if c[i] == ')' && c[i-1] == '('
dp[i] = 2;
假设前面还存在合法序列,如:
()()
dp[i] = dp[i-2] + 2;

b.包容关系的更新:
(())
if c[i] == ')' && c[i-1] == ')' && c[i-dp[i-1]-1] == '('
dp[i] = dp[i-1] + 2;
假设前面还存在合法序列,如:
()(())
dp[i] = dp[i-1] + 2 + dp[i-dp[i-1]-2]

这样,所有情况都考虑到了,我们在设置一个max随时更新最大即可。

代码如下:

public int longestValidParentheses(String s) {
        int n = s.length();
        int[] dp = new int[n];

        char[] c = s.toCharArray();
        int max = 0;
        for (int i = 0; i < n; i++){
            if (c[i] == ')'){
                if (i != 0 && c[i-1] == '('){
                    dp[i] = ((i - 2) != -1 ? dp[i-2] : 0) + 2;
                }

                if (i != 0 && c[i-1] == ')'){
                    if (i - dp[i-1] -1 != -1 && c[i-dp[i-1]-1] == '('){
                        dp[i] = dp[i-1]+2 + (i -dp[i-1]-2 != -1 ? dp[i-dp[i-1]-2] : 0);
                    }
                }

                max = Math.max(max, dp[i]);
            }
        }

        return max;
    }

总结:

  • 注意括号的等价条件,左括号个数等于右括号个数。
  • 括号相关都可以用stack进行抵消操作。
  • 注意最本质的两个元关系,【包容】和【合并】,只要能够设计出有效的算法满足它们即能得到有效解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值