最近一直在学习Deep Frist Search,也在leetcode上练习了不少题目。从最开始的懵懂,到现在遇到问题基本有了思路。依然清晰的记得今年2月份刚开始刷题的时做subsets的那个吃力劲,脑子就是转不过来到底该如何的递归,甚至试过使用debugger一步步的来看看堆栈到底是如何调用和返回的。经过了几个月的训练后,答题有了一些心得,记录下来,作为总结。
递归函数的整体结构:
返回值。进入入递归函数的第一件事就是写出递归终止的条件。通常递归终止的条件分为两类:
(1)有一个深度的标准,例如N-Queen问题,当我们递归的检测完第N个皇后,就到达来递归返回的条件,此时,将此次递归的结果作为一个解决方案存储。
(2)到达一个序列(数组/字符串)vector/string.size()的位置,例如subsets,permutation和word break ii的问题,当传递的参数已经无法索引数组/字符串的值时退出递归。
是否进行prune的操作。这个操作并不是一定会存在的。当需要prune的时候,通常是遇到了效率问题。当输入问题的规模非常大,存储之前已经做过检测或者得出结果的递归位置会大大降低递归的工作量。这个时候我们通常会利用一些数据结构。最简单的就是一个数组,复杂一点的会是一个hash table(word break ii in leetcode)。
本层的递归处理以及下一层次的递归调用。这一部分是递归的核心,如果这一部分模型处理正确,递归的调用就成功了90%(很像如果能写出动态规划的状态方程的感觉)。
返回值。最后的return,连同递归的处理和调用,具体内容会这下面详细阐述。
递归函数的类型:
总的来说DFS的函数分为两种类型,有返回值和没有返回值。递归的层次都是通过一个参数传递到函数内部,这两种函数的编写有区别,虽然都是DFS,但思维方式却是不一样。
对于无返回值的DFS函数,通常我们是将需要获取的值作为引用类型的参数传递到函数内部,当到达DFS函数终止的条件时我们获取想要得到的值,例如N-Queen,subsets, permutations等经典的问题。这是一种从上向下的思维模式。这种函数适用于求所有可能的解的问题。通常是先对该层的问题进行处理,然后再进入下一个层次的递归。当到达递归终止的条件时存储结果。然后再逐层的返回,通常会恢复该层这遍历之前的状态,为的是下一个递归。
以下为这种递归的模板:
void DFS_no_return(vector& solution_set, TYPE& solution, int idx){// DFS terminate condition// Normally there are two major categories// One is for some number, already reach this depth according to the question, stop// get to the end of an array, stopif(idx == n || idx == (vector/string).size()){solution_set.push_back(solution);return;}// very seldom only when our program met some efficiency problem// do prune to reduce the workload// no need do DFS if we already get the value of current element, just returnif(hash_table[input] != hash_table.end()){return;}for(int i = 0; i < n; i++){if(visited[i] == 0){// do something for current level processvisited[i] = 1;// for next level DFSDFS_no_return(solution_set, solution, idx + 1);// return from lower level DFS and restore the status for next DFSvisited[i] = 0;}}return; }
对于有返回值的DFS来说,我们需要获得的值是递归函数的返回值。与无返回值的递归函数相比,最大的一个区别是该类递归函数在递归调用返回之后处理该层的递归值。将从低层次递归返回的值结合本层的值处理,然后再返回给上一层递归函数调用。这种问题的思路是由上到下,再由下向上。通常是将一个复杂的问题逐层的分解成小问题,等到无法再分则返回,再将小问题拼装,然后逐层向上,返回大问题作为问题的解。通常在使用分治的思想时会使用有返回值的深度递归函数,这个应用在树中非常普遍。
以下为有返回值的递归函数的模板:
TYPE DFS_with_return(int idx){// DFS terminate condition// Normally there are two major categories// One is for some number, already reach this depth according to the question, stop// get to the end of an array, stopif(idx == n || idx == (vector/string).size()){return 1/0/"";}// very seldom only when our program met some efficiency problem// do prune to reduce the workload// no need do DFS if we already get the value of current element, just returnif(hash_table[input] != hash_table.end()){return hash_table[input];}TYPE ans;for(int i = 0; i < n; i++){if(visited[i] == 0){// normally, here should to add/concatenate the value in current level with the value returned from lower levelans += DFS_no_return(idx + 1);}}// if we need prune// keep current value herehash_table[input] = ans;return ans; }
总结:
- 递归函数的整体构造:
- 递归函数的分类:
- 无返回值:适用于求所有可能的解。
- 是一种由上而下,将大问题分解,在每一层提取出需要处理的小问题,然后向下递归,遇到递归终止的条件时,就得到一个问题的解,此时返回。
- 有返回值:使用与求个数/数量的问题。
- 是一种从上向下,将大问题分解到无法再分,然后再将分解后的小问题返回,逐层进行加工处理,然后再返回给上一层调用。
- 无返回值:适用于求所有可能的解。