leetcode 22 括号的生成 回溯法

leetcode 22 括号的生成 回溯法_第1张图片

/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int pos;
void getAns(char ** result, int left, int right, int *retNum, int n, char * map) {
    //printf("line:%d %d %d %d\n",__LINE__, left, right, *retNum);
    if (left == n && right == n) {
        result[*retNum] = (char *)calloc(1, 2 * n + 1);
        memcpy(result[*retNum], map, 2 * n + 1);
        //printf("return line:%d %d %s\n", __LINE__, *retNum, result[*retNum]);
        (*retNum)++;

        return;
    }
    if (left < n) {
        map[pos++]='(';
        //printf("line:%d pos:%d %s\n", __LINE__, pos, map);
        getAns(result, left + 1, right, retNum, n, map);
        pos--;
    }
    if (right < n && left > right) {
        map[pos++]=')';
        //printf("line:%d pos:%d %s\n", __LINE__, pos, map);
        getAns(result, left, right + 1, retNum, n, map);
        pos--;
    }
}
char ** generateParenthesis(int n, int* returnSize){
    if (n < 1) {
        *returnSize = 0;
        return NULL;
    }
    int num = 0;
    char ** ret = (char **)calloc(3000, sizeof(char *));
    char * map = (char *)calloc(1, 2 * n + 1);
    /*for (int i = 0; i < 3000; i++) {
        ret[i] = (char *)calloc(1, 2 * n + 1);
    }*/

    getAns(ret, 0, 0, &num, n, map);
    *returnSize = num;
    return ret;
}

 

果是递归数据结构,如单链表,二叉树,集合,则百分之百可以用深搜;如果是
非递归数据结构,如一维数组,二维数组,字符串,图,则概率小一些。
状态转换图:树或者图。
求解目标:必须要走到最深(例如对于树,必须要走到叶子节点)才能得到一个解,这种情况
适合用深搜。
10.7.2 思考的步骤
1. 是求路径条数,还是路径本身(或动作序列)?深搜最常见的三个问题,求可行解的总数,求
一个可行解,求所有可行解。
(a) 如果是求路径本身,则要用一个数组path[] 存储路径。跟宽搜不同,宽搜虽然最终求
的也是一条路径,但是需要存储扩展过程中的所有路径,在没找到答案之前所有路径都
不能放弃;而深搜,在搜索过程中始终只有一条路径,因此用一个数组就足够了。
(b) 如果是路径条数,则不需要存储路径。
2. 只要求一个解,还是要求所有解?如果只要求一个解,那找到一个就可以返回;如果要求所
有解,找到了一个后,还要继续扩展,直到遍历完。广搜一般只要求一个解,因而不需要考
虑这个问题(广搜当然也可以求所有解,这时需要扩展到所有叶子节点,相当于在内存中存
储整个状态转换图,非常占内存,因此广搜不适合解这类问题)。
152 第10 章深度优先搜索
3. 如何表示状态?即一个状态需要存储哪些些必要的数据,才能够完整提供如何扩展到下一步
状态的所有信息。跟广搜不同,深搜的惯用写法,不是把数据记录在状态struct 里,而是添
加函数参数(有时为了节省递归堆栈,用全局变量),struct 里的字段与函数参数一一对应。
4. 如何扩展状态?这一步跟上一步相关。状态里记录的数据不同,扩展方法就不同。对于固定
不变的数据结构(一般题目直接给出,作为输入数据),如二叉树,图等,扩展方法很简单,
直接往下一层走,对于隐式图,要先在第1 步里想清楚状态所带的数据,想清楚了这点,那
如何扩展就很简单了。
5. 关于判重
(a) 如果状态转换图是一棵树,则不需要判重,因为在遍历过程中不可能重复。
(b) 如果状态转换图是一个图,则需要判重,方法跟广搜相同,见第§9.6 节。这里跟第8 步
中的加缓存是相同的,如果有重叠子问题,则需要判重,此时加缓存自然也是有效果的。
6. 终止条件是什么?终止条件是指到了不能扩展的末端节点。对于树,是叶子节点,对于图或
隐式图,是出度为0 的节点。
7. 收敛条件是什么?收敛条件是指找到了一个合法解的时刻。如果是正向深搜(父状态处理完
了才进行递归,即父状态不依赖子状态,递归语句一定是在最后,尾递归),则是指是否达到
目标状态;如果是逆向深搜(处理父状态时需要先知道子状态的结果,此时递归语句不在最
后),则是指是否到达初始状态。
由于很多时候终止条件和收敛条件是是合二为一的,因此很多人不区分这两种条件。仔细区
分这两种条件,还是很有必要的。
为了判断是否到了收敛条件,要在函数接口里用一个参数记录当前的位置(或距离目标还有
多远)。如果是求一个解,直接返回这个解;如果是求所有解,要在这里收集解,即把第一步
中表示路径的数组path[] 复制到解集合里。
8. 如何加速?
(a) 剪枝。深搜一定要好好考虑怎么剪枝,成本小收益大,加几行代码,就能大大加速。这
里没有通用方法,只能具体问题具体分析,要充分观察,充分利用各种信息来剪枝,在
中间节点提前返回。
(b) 缓存。如果子问题的解会被重复利用,可以考虑使用缓存。
i. 前提条件:子问题的解会被重复利用,即子问题之间的依赖关系是有向无环图
(DAG)。如果依赖关系是树状的(例如树,单链表),没必要加缓存,因为子问题只
会一层层往下,用一次就再也不会用到,加了缓存也没什么加速效果。
ii. 具体实现:可以用数组或HashMap。维度简单的,用数组;维度复杂的,用
HashMap,C++ 有map,C++ 11 以后有unordered_map,比map 快。
拿到一个题目,当感觉它适合用深搜解决时,在心里面把上面8 个问题默默回答一遍,代码基
本上就能写出来了。对于树,不需要回答第5 和第8 个问题。如果读者对上面的经验总结看不懂或

感觉“不实用”,很正常,因为这些经验总结是笔者做了很多深搜题后总结出来的,从思维的发展过
程看,“经验总结”要晚于感性认识,所以这时候建议读者先做做后面的题目,积累一定的感性认识
后,在回过头来看这一节的总结,相信会和笔者有共鸣。

leetcode 22 括号的生成 回溯法_第2张图片

10.7.4 深搜与回溯法的区别
深搜(Depth-first search, DFS) 的定义见http://en.wikipedia.org/wiki/Depth_first_search,回溯法(backtracking)
的定义见http://en.wikipedia.org/wiki/Backtracking
回溯法= 深搜+ 剪枝。一般大家用深搜时,或多或少会剪枝,因此深搜与回溯法没有什么不同,
可以在它们之间画上一个等号。本书同时使用深搜和回溯法两个术语,但读者可以认为二者等价。
深搜一般用递归(recursion) 来实现,这样比较简洁。
深搜能够在候选答案生成到一半时,就进行判断,抛弃不满足要求的答案,所以深搜比暴力搜
索法要快。
10.7.5 深搜与递归的区别
深搜经常用递归(recursion) 来实现,二者常常同时出现,导致很多人误以为他俩是一个东西。
深搜,是逻辑意义上的算法,递归,是一种物理意义上的实现,它和迭代(iteration) 是对应的。
深搜,可以用递归来实现,也可以用栈来实现;而递归,一般总是用来实现深搜。可以说,递归一
154 第10 章深度优先搜索
定是深搜,深搜不一定用递归。
递归有两种加速策略,一种是剪枝(prunning),对中间结果进行判断,提前返回;一种是加缓
存(就变成了memoization,备忘录法),缓存中间结果,防止重复计算,用空间换时间。
其实,递归+ 缓存,就是一种memorization 。所谓memorization(翻译为备忘录法,见第
§13.1节),就是”top-down with cache”(自顶向下+ 缓存),它是Donald Michie 在1968 年创造的术语,
表示一种优化技术,在top-down 形式的程序中,使用缓存来避免重复计算,从而达到加速的目的。
memorization 不一定用递归,就像深搜不一定用递归一样,可以在迭代(iterative) 中使用
memorization 。递归也不一定用memorization,可以用memorization 来加速,但不是必须的。只有
当递归使用了缓存,它才是memorization 。
既然递归一定是深搜,为什么很多书籍都同时使用这两个术语呢?在递归味道更浓的地方,一
般用递归这个术语,在深搜更浓的场景下,用深搜这个术语,读者心里要弄清楚他俩大部分时候是
一回事。在单链表、二叉树等递归数据结构上,递归的味道更浓,这时用递归这个术语;在图、隐
士图等数据结构上,递归的比重不大,深搜的意图更浓,这时用深搜这个术语。

你可能感兴趣的:(leetcode 22 括号的生成 回溯法)