根据具体实例谈回溯算法
什么是回溯法
回溯算法也叫试探法,它是一种系统地搜索问题的解决方法,实际上是一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标,但当探索到某一步时,发现原先选择并不优或达不到目标时,就退回一步重新选择。
这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点成为“回溯点”。
回溯法适用问题
回溯法适用于一些树型结构的遍历问题,有些类似于树的DFS(深度优先搜索),即从头结点开始向下遍历,遇到不满足调节的节点,或是已构成一组解的节点即向上返回(“回溯”),然后继续按其他未访问过的节点向下遍历。所以通常回溯法都以递归来实现。
具体实例
leetcode17.电话号码的字母组合
题目描述:
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例:
输入:“23”
输出:[“ad”, “ae”, “af”, “bd”, “be”, “bf”, “cd”, “ce”, “cf”].
说明:
尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。
解题思路:
这显然是一个可以用树型结构来抽象的遍历问题。
给出一个图(这已经是尽我所能可以画出的似乎最好理解的图了<_<)
以输入数字为“234”为例,可以构建一个这样三层的结构。
仅以“a”开头的部分情况为例(即图中的红线部分)
从“a”开始向下遍历,到“d”,未满足终止条件,继续向下遍历到“g”,此时满足了终止条件,即字母个数与输入数字个数相等(都为3)。则回溯到字母“d”,继续向下走到“h”(通过循环实现),满足条件,再回溯到“d”,同样走到“i”,再回溯到“d”。
此时“d”之后的已经遍历完成。接下来应该是"e"了,用一个循环来实现“d”到“e”,同理当“e”,“f”都遍历完成时,回到“a”,从“a”到“b”同样通过循环来实现,所以在每次递归中加一层循环。
代码如下:
class Solution{
private:
vector<string> res;
vector<string> phone = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
public:
void dfs(string& digits,int idx,string tmp){
if(idx == digits.size()){
res.push_back(tmp);
return;
}
for(int i = 0;i < phone[digits[idx] - '0'].size();++i){
dfs(digits,idx + 1,tmp + phone[digits[idx] - '0'][i]);
}
}
vector<string> letterCombinations(string digits) {
if(digits.size() == 0) return res;
dfs(digits,0,"");
return res;
}
};
leetcode22. 括号生成
题目描述:
给出 n 代表生成括号的对数,请你写出一个函数,使其能够生成所有可能的并且有效的括号组合。
例如,给出 n = 3,生成结果为:
[
“((()))”,
“(()())”,
“(())()”,
“()(())”,
“()()()”
]
解题思路:
同样是一个树型问题,采用回溯算法来解决
去递归判断每一个分支,如果在某个节点,左括号的个数已经小于右括号的个数,显然括号不匹配,则回溯到上一个节点去判断下一个分支。如果未出现不匹配的情况,且左右括号的个数均等于输入整数n,则匹配成功,将该分支储存并回溯到上一节点,继续判断下一个分支。
代码如下:
class Solution {
public:
vector<string> generateParenthesis(int n) {
vector<string> res;
generateParenthesisDFS(n, n, "", res);
return res;
}
void generateParenthesisDFS(int left, int right, string out, vector<string> &res) {
if(left > right) return;
if(left == 0 && right == 0)
res.push_back(out);
else {
if(left > 0) generateParenthesisDFS(left - 1, right, out + "(", res);
if(right > 0) generateParenthesisDFS(left, right - 1, out + ')', res);
}
}
};
回溯法解题思路
试着概括一下解题思路 :)
1.将原问题抽象为树型的遍历问题
2.确定终止条件(该分支成功满足条件并记录或不满足条件且没有必要再该分支继续进行)
3.确定递归函数及其参数(参数一般每次递归要改变,如加上当前状态)
总结
感觉回溯法的难点是在如何构建参数来完成递归的构建。此算法在许多遍历求解集合的问题中都可以广泛使用。