【问题标题】:Understanding function to generate parentheses理解生成括号的函数
【发布时间】:2022-04-13 23:25:42
【问题描述】:

我有这个算法来生成格式正确的括号的所有组合。

有人能解释一下算法的核心概念吗?我尝试通过它进行调试,但我似乎仍然无法掌握算法背后的底层概念

此外,任何关于如何为这个问题提出这样一种算法的一般建议,即一个人是如何如此聪明地以这种方式解决它的,或者一个人必须做什么练习才能达到这个阶段。

问题:

给定n一对括号,编写一个函数来生成 格式良好的括号的所有组合。例如,给定n = 3,一个解集是:

“((()))”, “(()())”, “(())()”, “()(())”, “()()()”

代码:

public ArrayList<String> generateParenthesis(int n) {
    ArrayList<String> solutions = new ArrayList<String>();
    recursion(n, new String(), solutions);
    return solutions;
}

private void recursion(int n, String str, ArrayList<String> sol) {
    if(str.length() == 2 * n)
        sol.add(str);
    else {
        int left = 0;
        int right = 0;
        for(int i = 0; i < str.length(); ++i) {
            if(str.charAt(i) == '(')
                left++;
            if(str.charAt(i) == ')')
                right++;
        }
        if(left == right)
            recursion(n, str + "(", sol);
        else if(right < left) {
            if(left < n)
                recursion(n, str + "(", sol);
            recursion(n, str + ")", sol);
        }
    }
}

【问题讨论】:

标签: java algorithm


【解决方案1】:

它可以帮助我直观地看到调用是如何堆叠的。我在调用中添加了一个参数String depth,并在每次调用时打印出depth + str,为新调用的每个深度参数添加了四个空格。这让我们可以很好地了解调用顺序。

这是它的代码:

recursion(3, new String(), solutions, "");
//...
private static void recursion(int n, String str, ArrayList<String> sol, String depth) {
    System.out.println(depth + str);
    //...
        if(left == right)
            recursion(n, str + "(", sol, depth + "    ");
        else if(right < left) {
            if(left < n)
                recursion(n, str + "(", sol, depth + "    ");
            recursion(n, str + ")", sol, depth + "    ");
}

这是打印出来的:

(
    ((
        (((
            ((()
                ((())
                    ((()))
        (()
            (()(
                (()()
                    (()())
            (())
                (())(
                    (())()
    ()
        ()(
            ()((
                ()(()
                    ()(())
            ()()
                ()()(
                    ()()()

每一级递归都会在输出中添加另一个缩进。如果两个输出处于相同的缩进级别,那么它们都是从相同的递归级别调用的。

这是另一个视觉效果:

请注意,每个节点都是更深层次的递归,每次子节点直接从父节点中出来时,它不会分成两个递归路径。即父节点只调用一次recursion

【讨论】:

    【解决方案2】:

    递归肯定会让你头疼。这是另一种可能更容易遵循的方法:

    void generate() {
        ArrayList<String> results = new ArrayList<String>();
        generateParentheses(4, 0, new StringBuilder(), results);
        System.out.println(results);
    }
    
    void generateParentheses(final int remaining, final int openCount, final StringBuilder s, final List<String> results) {
        if (remaining == 0 && openCount == 0) {
            results.add(s.toString());
            return;
        }
        if (openCount > 0) { // we can close the open one
            s.append(")");
            generateParentheses(remaining, openCount-1, s, results);
            s.setLength(s.length()-1); // pop the last char off
        }
        if (remaining > 0) { // start a new one
            s.append("(");
            generateParentheses(remaining-1, openCount+1, s, results);
            s.setLength(s.length()-1); // pop the last char off
        }
    }
    

    输出为[()()()(), ()()(()), ()(())(), ()(()()), ()((())), (())()(), (())(()), (()())(), (()()()), (()(())), ((()))(), ((())()), ((()())), (((())))]

    这是从另一端解决问题的。你是怎么想出这些模式的?

    从对数开始 (remaining)。

    只有两种可能:打开或关闭。仅当还有一些剩余要附加时,才能附加开括号。只有当有相应的左括号要关闭时,才能附加右括号。

    因此,您只需要计算剩余的数量以及括号的深度即可。让递归处理剩下的事情。

    【讨论】:

      【解决方案3】:

      这是我对所提供算法的尝试分解:

      if(str.length() == 2 * n)
              sol.add(str);
      

      如果字符串长度是括号对数的两倍,您就知道您已经完成了。为什么?

      因为每对括号有 2 个字符长,所以有 n 对括号意味着您总共有 2 * n 个字符(即length / 2 == number of parentheses

      int left = 0;
      int right = 0;
      for(int i = 0; i < str.length(); ++i) {
          if(str.charAt(i) == '(')
              left++;
          if(str.charAt(i) == ')')
              right++;
      }
      

      这会逐个字符地遍历字符串,并根据特定字符是左括号还是右括号进行一些测试和更改leftright。基于此,你能弄清楚循环结束时的leftright是什么吗?

      leftright 分别是字符串中开/闭括号的数量

      if(left == right)
          recursion(n, str + "(", sol);
      else if(right < left) {
          if(left < n)
              recursion(n, str + "(", sol);
          recursion(n, str + ")", sol);
      }
      

      如果left == right,则调用相同的函数,除了在现有字符串上添加一个左括号。为什么要加括号,为什么是左括号?

      你知道你还没有完成,因为如果你完成了,你会触发第一个if 语句。由于没有未闭合的左括号(请记住,left == right),添加右括号会导致格式错误的括号,因此接下来的逻辑步骤是添加左括号。

      如果right &lt; left,您知道您至少有一个未闭合的左括号。所以你想再做一次检查。

      if(left < n)
          recursion(n, str + "(", sol)
      

      为什么要检查?如果left &lt; n,那么您知道如果关闭所有左括号,您将没有足够的括号对(n 括号对有n 左括号)。所以还不如再加一个左括号!

      最后一个语句有一个隐含的else 与之关联。如果left 不是&lt; n,那么您知道添加另一个左括号会使您超过请求的对数。所以添加右括号并继续。

      【讨论】:

        【解决方案4】:

        核心理念:

        1)X:如果一个字符串的右括号比左括号多,则在右侧添加更多括号不会使其成为格式良好的组合。

        2) 所有格式正确的组合都有相同数量的左括号和右括号。因此,每种类型都可以精确找到 n 次

        3)如果右括号的数量小于开括号的数量,那么我们总是可以通过添加更多的右括号来形成一个合式的结果。

        此算法构建组合,在右侧添加新符号

               public ArrayList<String> generateParenthesis(int n) {
                        ArrayList<String> solutions = new ArrayList<String>();
                        recursion(n, new String(), solutions);
                        return solutions;
                    }
        
                    private void recursion(int n, String str, ArrayList<String> sol) {
                    //If we got a sting long enough, we return it. This means a) We generate all 
        //strings only once. b)If a string of length 2*n is created, then it is correct. Other
        //code should satisfy these conditions
                        if(str.length() == 2 * n) 
                            sol.add(str);
                        else {
                            int left = 0;
                            int right = 0;
                            for(int i = 0; i < str.length(); ++i) {
                                if(str.charAt(i) == '(')
                                    left++;
                                if(str.charAt(i) == ')')
                                    right++;
                            }
                            //left and right are now numbers of parentheses in the string. 
        //Opening and closing respectively.
                            if(left == right)//On each step we maintain the condition X 
        //that the number of closing brackets is less or equal to the number of opening.
        //Therefore, is their numbers are equal we can only add opening ones
                                recursion(n, str + "(", sol);
                            else if(right < left) { // if X is maintained and we can add 
        //both types
                                if(left < n)// The number of opened should be no more than n, 
        //according to 2)
                                    recursion(n, str + "(", sol);
                                recursion(n, str + ")", sol);//The number of closing ones is 
        //limited by X by the number of opening ones, which is limited by n => the number of 
        //the closed ones is limited by n => we can add them as long as it doesn't violate X
                            }
        
                        }
                    }
        

        【讨论】:

          【解决方案5】:

          我以不同的风格编写了自己的递归括号生成器。它基本上构建了一个字符串,但在每次递归调用时,都会创建一个新字符串,以便回溯是正确的。我希望有人觉得它有帮助。

          import java.util.ArrayList;
          import java.util.List;
          
          public class GenerateParentheses {
          
              // N: The max number of matching parentheses. This value does not change.
              // usedL, usedR : Number of left and right parentheses already used in 'current' string.
              // current: the current string being built.
              // depth: recursion depth, used for pretty-printing
              public static void generate(int N, int usedL, int usedR, String current, List<String> result, int depth) {
          
                  System.out.printf("%susedL=%d, usedR=%d, current='%s'\n",
                              getIndentation(depth), usedL, usedR, current);
          
                  if (usedL == N && usedR == N) {
                      // We've used up all the available parentheses (up to N),
                      // so add the current built string to the result.
                      result.add(current);
                      return;
                  }
          
                  if (usedL < N) {
                      // Add another left parenthesis "(".
                      String newCurrent = current + "(";
                      generate(N, usedL + 1, usedR, newCurrent, result, depth+1);
                  }
          
                  if (usedR < N && usedL > usedR) {
                      // Add another right parenthesis ")" if there are already 
                      // used left parentheses.
                      String newCurrent = current + ")";
                      generate(N, usedL, usedR + 1, newCurrent, result, depth+1);
                  }
              }
          
              // Utility function used for pretty-printing.
              private static String getIndentation(int depth) {
                  StringBuilder sb = new StringBuilder();
                  for (int i = 0; i < depth; i++) {
                      sb.append("  ");
                  }
                  return sb.toString();
              }
          
              public static void main(String argv[]) {
          
                  int N = 3;
                  int usedL = 0;
                  int usedR = 0;
                  String current = "";
                  List<String> result = new ArrayList<String>();
                  int depth = 0;
          
                  generate(N, usedL, usedR, current, result, depth);
          
                  for (String s : result) {
                      System.out.printf("%s\n", s);
                  }
              }
          }
          

          这是输出:

          usedL=0, usedR=0, current=''
            usedL=1, usedR=0, current='('
              usedL=2, usedR=0, current='(('
                usedL=3, usedR=0, current='((('
                  usedL=3, usedR=1, current='((()'
                    usedL=3, usedR=2, current='((())'
                      usedL=3, usedR=3, current='((()))'
                usedL=2, usedR=1, current='(()'
                  usedL=3, usedR=1, current='(()('
                    usedL=3, usedR=2, current='(()()'
                      usedL=3, usedR=3, current='(()())'
                  usedL=2, usedR=2, current='(())'
                    usedL=3, usedR=2, current='(())('
                      usedL=3, usedR=3, current='(())()'
              usedL=1, usedR=1, current='()'
                usedL=2, usedR=1, current='()('
                  usedL=3, usedR=1, current='()(('
                    usedL=3, usedR=2, current='()(()'
                      usedL=3, usedR=3, current='()(())'
                  usedL=2, usedR=2, current='()()'
                    usedL=3, usedR=2, current='()()('
                      usedL=3, usedR=3, current='()()()'
          ((()))
          (()())
          (())()
          ()(())
          ()()()
          

          【讨论】:

            【解决方案6】:

            随着您获得更多递归经验,您将更容易了解如何获得解决方案。

            想法:格式正确的括号总是以左括号开头,左右的数量相等,从左到右阅读时,以下总是正确的left &gt;= right

            因此,在制定递归解决方案时,我们将使用一个简单的规则:prefer to open left paren,然后让递归函数展开的性质处理其余的事情

            private void recursion(int n, String str, ArrayList<String> sol) {
                if(str.length() == 2 * n)
                    sol.add(str);
            

            我们有 n 对,添加解决方案并返回

                else {
                    int left = 0;
                    int right = 0;
                    for(int i = 0; i < str.length(); ++i) {
                        if(str.charAt(i) == '(')
                            left++;
                        if(str.charAt(i) == ')')
                            right++;
                    }
            

            计算左右括号的数量

                    if(left == right)
                        recursion(n, str + "(", sol);
            

            str 当前是平衡的,因为我们更喜欢左而不是右,所以添加一个左

                    else if(right < left) {
            

            这可能只是一个 else,right 永远不会是 > left。无论哪种方式,这意味着我们目前不平衡,至少左边比右边多 1 个。

                        if(left < n)
                            recursion(n, str + "(", sol);
            

            检查是否可以添加另一个左侧,因为更喜欢左侧而不是右侧

                        recursion(n, str + ")", sol);
                    }
                }
            

            添加我们的右括号。这将关闭添加在其上方行中的括号,或者如果未执行,它将关闭较早的左侧(请记住,我们在此块中,因为它当前不平衡)

            【讨论】:

            • 我很难想象它在 n = 3 上生成的第二个和第三个字符串;如果你调试它,它看起来有点奇怪。
            • @ShahrukhKhan 从第一个分组开始,然后从该点跳入代码。所有右括号展开,当str为'(('时,我们从recursion(n, str + "(", sol);返回(第三个(在调用中添加,未添加到本地str)。下一行表示添加右括号,它来自那里
            【解决方案7】:

            这里有一个更简单、更直观的解决方案。

            同样,这遵循递归的想法,但它比您发布的更容易阅读和更有效。

            public void generateParantheses(int n){
                helper(n,0,0,"");
            }
            public void helper(int n, int open, int close, String result){
            
                if(result.length()==2*n) { 
                // easy enough to understand? one open and one close for each n?
                    System.out.println(result);
                    return;
                }
                if(open<n){
                    //start off with all n open parantheses
                    helper(n, open+1, close, result+"(" );
                }
                if(close<open){
                    // now check if we can add closing parantheses on top of open in this condition
                    helper(n, open, close+1, result+")");
                }
            }
            

            【讨论】:

              【解决方案8】:
              public static void findParenthisis(String s , int left ,int right){
                      if(left==right && left == 0){
                          System.out.println(s);
                      }
                      if(left > 0){
                          findParenthisis(s+'(',left-1,right);
                      }
                      if(left < right){
                          findParenthisis(s + ')',left,right-1);
                      }
                  }
              

              【讨论】:

                【解决方案9】:
                    void generateParenthesis(int open, int close, int position, int n, char[] str) {
                    /*
                     * open  = open parenthesis
                     * close = close parenthesis
                     * position   = 2*n (length of combination of valid parenthesis
                     * n     = pair of parenthesis
                     * Algorithm:
                     * 1.Check if position == 2*n --  Yes, print the str
                     * 2.check if open is less than n 
                     *      If true, add a open parenthesis into str and  call the function recursively by 
                     *      incrementing open by 1 and position by 1
                     * 3.check if close < open
                     *      If true , add a close parenthesis and call the function recursively by 
                     *      incrementing close by 1 and position by 1*/
                
                
                    if(position ==str.length) {
                        for(int i=0;i<str.length;i++) {
                            System.out.print(str[i]);
                        }
                        System.out.println();
                        return;
                    }
                    if(open < n) {
                        str[position] = '(';
                        generateParenthesis(open+1,close,position+1,n,str);
                
                    }if(close< open) {
                        str[position]=')';
                        generateParenthesis(open,close+1,position+1,n,str);
                    }
                
                }
                

                【讨论】:

                  【解决方案10】:

                  我想我找到了一个非常直观的解决方案,可以从类似问题中获得灵感。想象一下,你有一个大小为 n x n 的网格。您想从左下角移动到右上角。向右的每一步都可以解释为(,向上的每一步都可以解释为)。最简单的情况是 1x1 的网格。有两种方式:RU(右后上)和UR(上后右)第一个对应(),第二个对应)(。第二种情况是无效的,所以我们丢弃了网格上三角形的所有路径。对于n=4

                  问题可以递归解决。我们应该从R=0, U=0开始。一旦我们到达R=n, U=n,我们就完成了,应该将partial_solution 添加到solutions。有两种情况:

                  1. R&lt;n 时,我们仍然可以向右移动并递增R
                  2. U&lt;R 注意到时,我们只能在对角线以下的情况下上升。在这种情况下,我们仍然可以向上移动并增加 U
                  def move(R, U, n, solutions, partial_solution):
                  
                      if R == n and U == n:
                          solutions.append(partial_solution)
                          return
                  
                      if R < n:
                          move(R + 1, U, n, solutions, partial_solution + '(')
                      if U < R:
                          move(R, U + 1, n, solutions, partial_solution + ')')
                  
                  solutions = []
                  n=4
                  move(0, 0, n, solutions, '')
                  print(solutions)
                  

                  补充:

                  请注意,这个公式还有助于我们理解形成括号的方法的数量。从R=0, U=0R=n, U=n 需要一个RU 的字符串,并且我们应该有n 个R 和n 个U 的属性。例如对于 n=3:

                  RRRUUU, RRURUU, URURUR, URRRUU, ...
                  

                  这是一个组合问题,方法的数量是combination(2n, n)。但是我们必须考虑去掉上面的三角形。这给我们留下了1/(n+1) * combination(2n, n),这是nCatalan 编号。

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 2019-08-11
                    • 2018-02-07
                    • 1970-01-01
                    • 2018-12-28
                    • 2018-08-27
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多