1 定义
回溯是递归的一种形式,同时也是递归的副产品,也就是说只要有递归就会有回溯。在遇到必须从多种选项中选择一项的时候,你选择了一项后又会出现多种选项供选择,直到选择到了“正确的选项”函数才结束。因此,回溯法解决的问题都可以抽象为树形结构(不一定是二叉树)。
回溯虽然是一种非常典型的递归算法,但是任何递归算法都可以重写为堆栈算法。事实上,这就是递归算法被翻译成机器或汇编语言的方式。
2 算法思想
回溯法就是暴力搜索,并不是什么高效的算法,最多再剪枝一下(dfs的优化)。因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。因为用到了递归,是递归就必须要有终止条件(basecase),在树状结构中,这棵树(N叉树)的高度必须是有限制。
在此,我引入“原问题”和“子问题”这两个概念来帮助大家理解回溯。题目要求解决的问题,可以看成是一个原问题。而在这整个原问题中,其中的一个选项就可以看作是一个子问题,如此循环往复:该子问题本身,又可以看成是一个新的原问题。
3 代码模板
3.1 递归实现
void trackback(参数) { if (终止条件) { 存放结果; return; } for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { 处理节点; trackback(路径,选择列表); // 递归 pop//回溯,撤销处理结果 } }
在这其中,这个for循环本身是一个横向遍历,也就是说一个根节点的下一层有多少个子节点,这个循环就会执行多少次;for循环中的trackback函数调用就是一个递归的过程,是一个纵向遍历,也就是说这棵树有多少层,这个函数就会递归多少次。pop的这个操作就是回溯。
3.2 栈实现
bool solve(Node& n) { 创建一个栈stack; 将节点n压入stack; while(!stack.empty()) { if 栈顶部是叶子节点 { if(该节点是目标节点) return true; else stack.pop(); } else { if 栈顶部的节点有未尝试的子节点 将下一个未尝试的子节点推到栈上; else 把它从栈顶弹出; } return false; }
4 例题
4.1 例题1
【leetcode-17】给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
4.1.1 题目分析
首先使用哈希表存储每个数字对应的所有可能的字母,然后进行回溯操作。
回溯过程中维护一个字符串,表示已有的字母排列(如果未遍历完电话号码的所有数字,则已有的字母排列是不完整的)。该字符串初始为空。每次取电话号码的一位数字,从哈希表中获得该数字对应的所有可能的字母,并将其中的一个字母插入到已有的字母排列后面,然后继续处理电话号码的后一位数字,直到处理完电话号码中的所有数字,即得到一个完整的字母排列。然后进行回退操作,遍历其余的字母排列。
回溯算法用于寻找所有的可行解,如果发现一个解不可行,则会舍弃不可行的解。在这道题中,由于每个数字对应的每个字母都可能进入字母组合,因此不存在不可行的解,直接穷举所有的解即可。
4.1.2 代码实现
class Solution { public: vectorletterCombinations(string digits) { vector combinations; if(digits.empty()){ return combinations; } unordered_map phoneMap{ {'2', "abc"}, {'3', "def"}, {'4', "ghi"}, {'5', "jkl"}, {'6', "mno"}, {'7', "pqrs"}, {'8', "tuv"}, {'9', "wxyz"} }; string combination; traceback(combinations,combination,0,phoneMap,digits); return combinations; } void traceback(vector & combinations,string& combination,int i,unordered_map & phoneMap,string& digits){ if(i == digits.size()){ combinations.push_back(combination); return; }else{ char digit = digits[i]; auto letters = phoneMap[digit]; for(auto letter:letters){ combination.push_back(letter); traceback(combinations,combination,i+1,phoneMap,digits); combination.pop_back(); } } } };
4.2 例题2
【leetcode-144】给你二叉树的根节点 root ,返回它节点值的 前序 遍历。
4.2.1 题目分析
这道题可以用递归暴力获取答案,但是,这里给出一种用栈实现的更为巧妙的解法,其结构与bfs中的基本模板有些类似。只不过,在栈中,数据履行“先进后出”的原则,使用在将子节点压入栈是,要先压右节点,后压左节点。
4.2.2 代码实现
class Solution { public: vectorpreorderTraversal(TreeNode* root) { vector ans; if(!root){ return ans; } stack s; s.push(root); while(!s.empty()){ auto node = s.top(); s.pop(); ans.push_back(node->val); if(node->right){ s.push(node->right); } if(node->left){ s.push(node->left); } } return ans; } };