回溯法
通过枚举法,对所有可能性进行遍历。 但是枚举的顺序是 一条路走到黑,发现黑之后,退一步,再向前尝试没走过的路。直到所有路都试过。因此回朔法可以简单的理解为: 走不通就退一步的方枚举法就叫回朔法。而这里回退点也叫做回朔点。
- 回朔关键点 通过分析发现,回朔法实现的三大技术关键点分别是:
一条路走到黑
回退一步
另寻他路 - 关键点的实现 那么如何才能用代码实现上述三个关键点呢?
for 循环
递归 - 解释如下
for循环的作用在于另寻他路: 你可以用for循环可以实现一个路径选择器的功能,该路径选择器可以逐个选择当前节点下的所有可能往下走下去的分支路径。 例如: 现在你走到了节点a,a就像个十字路口,你从上面来到达了a,可以继续向下走。若此时向下走的路有i条,那么你肯定要逐个的把这i条都试一遍才行。而for的作用就是可以让你逐个把所有向下的i个路径既不重复,也不缺失的都试一遍
递归可以实现一条路走到黑和回退一步: 一条路走到黑: 递归意味着继续向着for给出的路径向下走一步。 如果我们把递归放在for循环内部,那么for每一次的循环,都在给出一个路径之后,进入递归,也就继续向下走了。直到递归出口(走无可走)为止。 那么这就是一条路走到黑的实现方法。 递归从递归出口出来之后,就会实现回退一步。
因此for循环和递归配合可以实现回朔: 当递归从递归出口出来之后。上一层的for循环就会继续执行了。而for循环的继续执行就会给出当前节点下的下一条可行路径。而后递归调用,就顺着这条从未走过的路径又向下走一步。这就是回朔。
说了这么多,回朔法的通常模板是什么呢? 递归和for又是如何配合的呢?
def backward():
if (回朔点):# 这条路走到底的条件。也是递归出口
保存该结果
return
else:
for route in all_route_set : 逐步选择当前节点下的所有可能route
if 剪枝条件:
剪枝前的操作
return #不继续往下走了,退回上层,换个路再走
else:#当前路径可能是条可行路径
保存当前数据 #向下走之前要记住已经走过这个节点了。例如push当前节点
self.backward() #递归发生,继续向下走一步了。
回朔清理 # 该节点下的所有路径都走完了,清理堆栈,准备下一个递归。例如弹出当前节点
39.组合总和
回溯
var combinationSum = function(candidates, target) {
let result = [];
let len = candidates.length;
// 路径
let path = [];
if (len == 0) return result;
// 优化: 排序数组
// candidates.sort((a, b) => a-b);
fn(candidates, 0, target, path);
console.log(result);
return result;
function fn(candidates, start, target, path) {
if (target < 0) return;
if (target == 0) {
result.push([...path]);
return;
}
// start避免重复,只向后找
for(let i=start; i < candidates.length; i++) {
if (target >= candidates[i]) {
path.push(candidates[i]);
fn(candidates, i, target-candidates[i], path);
// 回退一步
path.pop();
}
}
}
};
动态规划
var combinationSum = function(candidates, target) {
let dp = [];
for(let i = 1; i <= target; i++) {
dp[i] = [];
for(let j = 0; j < candidates.length; j++) {
if (i == candidates[j]) {
dp[i].push([candidates[j]]);
}
if (i > candidates[j]) {
for(let k = 0; k < dp[i-candidates