Leetcode分类解析:组合算法
所谓组合算法就是指:在解决一些算法问题时,需要产生输入数据的各种组合、排列、子集、分区等等,然后逐一确认每种是不是我们要的解。从广义上来说,组合算法可以包罗万象,甚至排序、各种搜索算法都可以算进去。最近读《The Algorithm Design Manual》时了解到这种归类,上网一查,甚至有专门的书籍讲解,而且Knuth的巨著TAOCP的第四卷就叫组合算法,看来还真是孤陋寡闻了!于是最近着重专攻了一下Leetcode中所有相关题目,在此整理一下学习心得。题量重要,质量也重要!
1.分类地图
个人以为,以组合算法为一大类是非常好的分类方式,比目前网上看到的一些类似穷举、BFS、DFS的分类方法要清晰得多。那首先来看一下组合算法在本系列所处的位置,以及它可以细分为几小块吧:
- 基础结构(Fundamentals)
1.1 数组和链表(Array&List):插入、删除、旋转等操作。
1.2 栈和队列(Stack&Queue):栈的典型应用。
1.3 树(Tree):构建、验证、遍历、转换。
1.4 字符串(String):转换、搜索、运算。 - 积木块(Building Block)
2.1 哈希表(Hashing)
2.2 分治(Divide-and-Conquer)
2.3 排序(Sorting)
2.4 二分查找(Binary Search) - 高级算法(Advanced):
3.1 组合算法(Combinatorial Algorithm):
- 回溯(Backtracking)
- 组合(Combination)
- 子集(Subset)
- 排列(Permutation)
- 分区(Partition)
3.2 贪心算法(Greedy Algorithm):贪心的典型应用。
3.3 动态规划(Dynamic Programming):广泛应用DP求最优解。 - 其他杂项(Misc):
4.1 数学(Math)
4.2 位运算(Bit Manipulation)
4.3 矩阵(Matrix)
2.解题策略
关于组合算法的解题策略,红宝书《The Algorithm Design Manual》的第7章和第14章有详细的介绍。如果还嫌不够的话,可以参考Knuth的宏篇巨著《The Art of Computer Programming》4a卷。回溯是列举所有可能解来实现组合算法的典型技术。《The Algorithm Design Manual》除了基本问题外,还介绍了一些巧妙的剪枝技术,在此就不涉及了,还是以Leetcode为蓝本,避免跑题。
2.1 递归调用、深度搜索和回溯技术
初学时,难免对回溯、DFS、递归三者的关系理不清,感觉好像都是一个东西,其实不然。要想理清这三者的关系,先递归,再DFS/BFS,最后看一下回溯就一目了然了。
2.1.1 递归(Recursion)
递归可以用来实现各种符合递归结构的算法(正在专门写一篇递归的文章《程序设计基石:递归》)。从实现机制上来说,递归只是编程语言提供给我们的一种程序编写方式,在操作系统运行程序时用栈Frame来帮我们实现。从解决问题的方式上来说,与循环从前往后的解决问题方式类似,递归是自底向上,先得到更小问题的解,再逐步合并成大问题的解。以Leetcode习题为例,最典型的就是递归实现的Divide-and-Conquer策略,能够解决一大类问题,所以它绝不限于DFS和回溯问题。
2.1.2 深度/广度优先搜索(DFS/BFS)
而所谓的深度优先DFS、广度优先BFS,则是属于图(树)范畴的术语,特指逐步遍历图中各结点的方式。一般来说,DFS用递归实现比较方便,因为我们充分利用编程语言提供的便利,将Stack的维护问题交给OS去管理了。当然我们完全可以忽视这种遍历,自己显示用循环+Stack方式实现。而BFS则一般要用循环+Queue的方式去实现,因为没有像递归那样的简便实现机制,所以稍显麻烦一些。以Leetcode习题为例,树的前中后序遍历都属于DFS,而Level序遍历(及像ZigZag各种变种问题)都属于BFS。
2.1.3 回溯(Backtracking)
终于说到了回溯,《The Algorithm Design Manual》给出了定义:”Backtracking can be viewed as a depth-first search on an implicit graph”,用树(图)来表示递归的执行过程的话(这是很自然的研究递归的方式),回溯就是在隐式图上执行的DFS搜索来构建解的方式。所谓隐式图在第5章开头有解释:它指的就是我们不会一上来就把回溯的整个递归过程形成的树(图)完全生成出来,而是随着回溯的执行,一点点构建,有点像游戏里的打地图。其实很容易理解,因为很多问题不是要找到所有解,而是判断有解就可以结束回溯了,所以没必要走完整个搜索空间。当然,上面的定义并不绝对,为什么不用BFS呢?《The Algorithm Design Manual》的解释是:因为对大部分问题来说,执行过程树的高度不会太高