zoukankan      html  css  js  c++  java
  • LeetCode (22): Generate Parentheses

    链接: https://leetcode.com/problems/generate-parentheses/

    【描述】

    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:

    "((()))", "(()())", "(())()", "()(())", "()()()"

    【中文描述】

    给定一个数字n,要求写一个方法,生成所有n个括号的可能合法组合。

    例如,n=3, 那么3个括号的所有可能组合如上。

    ————————————————————————————————————————————————————————————

     

    【初始思路】

    基本素养,只要见到求所有可能组合的情况(所有可能组合的个数), 首先立马想几个概念词:DFS、递归、回溯。有了这个直觉,你就省去了大量的时间,直接往递归上去套。接下来就是考虑怎么设计递归函数的问题了,速度自然会快很多。

    这个题也是如此,回溯、递归。

    怎么递归。

    我开始的想法是,既然是合法组合,我干脆每次递归的时候直接选择一整个"()"组合作为递归添加项目就可以了。这样子不用考虑不合法的情况了。例如,要求2个括号的合法组合,那么先给一个括号,然后在这个括号的基础上,加第二个括号,无非有如下3种加法,我们用数字来表示可以填入第二个括号的位置:

                                                     0 1 2

    也就是说,我们可以在0、1、2三个位置填入第二个括号,就能生成所有组合了。当然,填入2和填入0的效果是一样,后面讨论如何去重这种情况。

    如果如题目要求,n=3时,那么可能的填入情况如下:

                                        

    上图中,2衍生出来的那一支根本就不用再走,明显是重复的。所以没有画出其衍生的结果。

     

    这其实是个树, 叶子就是所有我们能够生成的串的集合。每次生成可能的串后,检查下还需不需要加入括号,如果需要,继续递归进入下一层即可。如何控制基准条件?我们用一个int need来作为递归层数的控制(也即还需填入几个括号),当need=0的时候,显然当前串不需要再加入括号,直接返回一个空串给上层即可。如果need不为0,那么在生成当前串之后,在当前串基础上递归,need-1即可。

     

    那么,如何实现在0,1,2,3,4这样的位置填入括号呢?我用了substring方法,每次传入一个当前串s,递归方法在当前串的基础上动手脚,要把当前的括号加入0的位置,其实就是拿一个stringbuilder,先appen当前括号,然后再append(s(0位置后))。如果要在1的位置加括号,就是sb.append(s(0,1)), 再append当前括号,然后再append(s剩余串)。我们用一个for循环来实现这个功能,循环次数就是当前传入串s的长度。

     

    那么好了,讨论到这里,不去重的代码就可以写出来了。那么,如何去重呢?

     

    我们看上面的串,所有中间结果都会衍生出一个后续的结果。如果中间结果和之前已经生成过的串一样,那肯定不用往下走了,再走也是重复。基于这个思路,我用了一个HashMap,存入所有中间结果串,每次生成中间串的时候,在map里找找看,如果已经生成过这个串,直接continue考虑下一种可能性。如果没有,那么当前串入map,接着递归继续衍生。

     

    好了,代码如下。

    【Show me the Code!!!】

     1 /**
     2      * 主驱动程序
     3      * @param n
     4      * @return
     5      */
     6     public static List<String> generateParenthesis(int n) {
     7         List<String> list = new ArrayList<String>();
     8         if(n == 0) {
     9             list.add(new String(""));
    10             return list;
    11         }
    12         Map<String, Integer> map = new HashMap<String, Integer>(); //用于记录已经出现过的串
    13         return recursiveGenerator("", map, n);
    14     }
    15 
    16     /**
    17      * 递归主方法
    18      * 在已有串: para中找出各个位置, 分别插入(),并形成list返回
    19      * @param para 已有串
    20      * @param need 还需几个(), need==1时为基准条件
    21      * @return
    22      */
    23     public static List<String> recursiveGenerator(String para, Map<String, Integer> map, int need) {
    24         if (need == 0) {
    25             //base condition
    26             return new ArrayList<String>();
    27         } else {
    28             //non base condition
    29             //在para中找各个位置,加入一个(),然后直接返回
    30             List<String> list = new ArrayList<String>();
    31             for (int i = 0; i <= para.length(); i++) {
    32                 StringBuilder sb = new StringBuilder();
    33                 sb.append(para.substring(0,i))
    34                         .append("()").append(para.substring(i, para.length()));
    35                 //子结果去重
    36                 if(map.containsKey(sb.toString())) {continue;}
    37                 else {
    38                     map.put(sb.toString(), 1);
    39                 }
    40                 List<String> rec = recursiveGenerator(sb.toString(), map, need - 1);
    41                 if(rec.size() == 0) {
    42                     list.add(sb.toString());
    43                 } else {
    44                     for(String s : rec) {
    45                         list.add(s);
    46                     }
    47                 }
    48             }
    49             return list;
    50         }
    51     }
    generateParentheses

    可是这个代码还是笨,首先用了HashMap,耗空间不说,速度也可能会受到一定影响。果然,运行了一下, 8个用例跑了13ms。有没有更快的办法?

    【重整思路】

    换个角度考虑问题,我上面是把左右括号作为一个整体做考虑,然后插入的。我们能不能把左右括号拆开,从左往右写,看看能写出多少可能性?这其实是人类正常的思维方式。由于要写出所有的合法括号组合,先写左括号,左括号全部用完,再写右括号,这将是第一个最容易想到的可能性。然后呢?肯定需要回溯了。回溯到一个位置,我们不写"(",写一个")"然后再试着写写看。如此往复,所有的可能就全部写出来。此外,这个方法还完美解决了重复情况的可能性。因为每一次新的尝试,都是"新"的尝试,之前从来没有尝试过,所以不可能产生已经出现过的结果。具体可以看下面图帮助理解:

                                  

    上图可以清楚的看到n=3的时候,这种递归方案的全部过程,从左到右递归,直到紫色回溯的时候,再也找不到其他方案,最终回到最初,全部流程结束。

    具体实现,有几个核心的点需要注意。如何实现优先选择写"("? 如何控制回溯? 其实答案就隐含在上面的图片里。

    虚线箭头其实就是程序走到最终情况时候,往回一路返回的过程。没返回一次,程序需要检查一下是否还有其他合法情况,如果没有继续返回。直到找到合法情况,进入下一个合法情况。

    如何检查是否还有合法情况?很简单,合法括号的含义就是有几个左括号,就必然要对应给几个右括号。那么每一次返回之后,看看是否还有"("可以用,如果有,就用一个左括号,然后进入该情况递归。如果没有,就继续返回。所以,我们需要一个左括号计数器来判断是否还有可以用的左括号。那么如何判断走到最终结果了呢?对了!右括号计数器。当右括号和左括号同时用完的时候,我们就认为走到最终结果了,直接return。

    到这里就可以了么?如上图,紫色最终回溯到起点第一个左括号之后,按道理还可以继续回溯,然后第一个字符直接填入一个")", 结果岂不是就错了。而这种情况是显而易见会发生的。这其实就给我们了一个教训,递归方法不是只需要关注基准条件就可以的,还需要考虑某种情况下是否不合法!这个题就是最好的例子。按照通用逻辑,左括号先写的情况全部试过后,就得试右括号先写的情况了,这种情况下明显是不合法的。处理方法也很简单。每次递归的时候先检查一下剩余的右括号是否小于左括号,如果是的话,说明先写了右括号,不合法直接返回。

    最后,代码就可以写出来了。

    【Show me the Code!!!】

     1 public class OtherSolution {
     2     /**
     3      * 定义全局变量,无需再传值到各个方法
     4      */
     5     static List<String> list = new ArrayList<String>();
     6 
     7     public static List<String> generateParenthesis(int n) {
     8         /**
     9          * 初始状态,左括号和右括号都可以添加n个,所以左右都传了n
    10          */
    11         generateLeftsAndRights("",n,n);
    12         return list;
    13     }
    14 
    15     /**
    16      * DFS方法
    17      * 该方法的本质是,每次调用都深度优先用掉左括号,然后再用掉右括号.
    18      * @param subList 记录每次递归时候当前串, 方法将在subList上继续添加括号
    19      * @param left 左括号还剩下的个数
    20      * @param right 右括号还剩下的个数
    21      */
    22     private static void generateLeftsAndRights(String subList,int left, int right){
    23         /**
    24          * 最后可能出现的情况, 右括号比左括号先用完, 明显不是合法的串,应剔除
    25          */
    26         if(left > right) return;
    27 
    28         /**
    29          * 左括号还剩得有, 优先用左括号
    30          * 该方法将不断深搜, 直到左括号全部用完
    31          */
    32         if(left > 0){
    33             generateLeftsAndRights(subList + "(", left-1, right);
    34         }
    35 
    36         /**
    37          * 开始用右括号
    38          */
    39         if(right > 0){
    40             generateLeftsAndRights(subList + ")", left, right-1);
    41         }
    42 
    43         /**
    44          * 基准情况, 各自都用完, 说明已经形成了一个合法的串,将该串加入list返回
    45          */
    46         if(left == 0 && right == 0){
    47             list.add(subList);
    48             return;
    49         }
    50     }
    51 }
    generateParenthesis

    最后,重申,凡是遇到要求全部可能集合、可能性、可能性个数的题目,立马考虑递归回溯求解!!!

     

     

     

     

     

  • 相关阅读:
    转部机器人相关的电视剧
    斯坦福大学自然语言处理第一课“引言(Introduction)”
    丹尼斯·洪:讲述他的七种全地形机器人
    斯坦福大学自然语言处理公开课课件汇总
    大数据挖掘
    脑机接口技术真能破译我们的想法吗?
    现代战场 机器人的崛起
    斯坦福大学自然语言处理第三课“最小编辑距离(Minimum Edit Distance)”
    MIT开放式课程“自然语言处理”介绍
    斯坦福大学自然语言处理第六课“文本分类(Text Classification)”
  • 原文地址:https://www.cnblogs.com/lupx/p/leetcode-22.html
Copyright © 2011-2022 走看看