回溯法
参考 - 剑指Offer
回溯法可以看成蛮力法的升级版,它从解决问题每一步的所有可能选项里系统地选择出一个可行的解决方案.
回溯法解决的问题的特性:
-
可以形象地用树状结构表示:
- 节点: 算法中的每一个步骤
- 节点之间的连接线: 每个步骤中的选项,通过每一天连接线,可以到达下一个子步骤
- 叶子节点: 代表一个步骤的最终状态
-
如果在叶节点的状态满足需求,那么我们找到了一个可行的解决方案
-
如果在叶节点的状态不满足约束条件,那么只好回溯到它的上一个节点再尝试其他的选项。如果上一个节点所有可能的选项都已经试过,并且不能达到满足约束条件的终结状态,则再次回溯到上一个节点.如果所有节点的所有选项都已经尝试过仍然不能达到满足约束条件的终结状态,该问题无解
栗子 - 数组总和
题目参考 - 39.数组总和
算法思路:
- 变量:
- 使用len缓存当前数组的长度
- 使用path缓存当前的路径
- 使用res缓存要返回的结果
- 处理:
- 为了方便后续的剪枝操作,需先对数组进行排序
- 使用深度优先算法,传入3个参数: resides(离目标还差多少), path, begin(从哪一个开始添加)
- 每次进入时,判断一下,resides是否小于0:
- 是: return
- 否: 不做处理
- 之后判断resides是否等于0
- 是: 证明找到一个条符合要求的路径,将path推入res中(此处需特别注意,数组是JS中的引用类型.在后文中会回溯,最终的path是一个空数组. 如果直接将path推入res.其实是将path的内存地址推入res.最终会根据地址寻找到空数组.因此此处推入的是path.slice()). slice方法参考
- 否: 不做处理
- 到这里,循环遍历candidates数组
- 每次将当前的值推入路径
- 然后调用dfs函数
- dfs函数结束之后,要进行回溯操作,即将对path使用pop()方法
- 每次进入时,判断一下,resides是否小于0:
var combinationSum = function(candidates, target){let len = candidates.length,path = [],res = []candidates.sort((a,b)=>a-b)function dfs(resides, path, begin){if(resides < 0) returnif(resides == 0) return res.push(path.slice()) // 此处需要返回一个新的数组,不能使用同一个内存中的数组for(let i = begin; i < len ; i++){if(resides - candidates[i] < 0) break;path.push(candidates[i])dfs(resides - candidates[i], path, i)path.pop()}}dfs(target, path, 0)return res
}
注意: res.push(path)时,由于path是个引用类型.因此实际上push进的是一个十六进制地址.可以看做下面:
res[0] = ‘0xffffffff’
当后面回溯path.pop()时, 内存0xffffffff中的值会改变.
最后一次回溯 内存0xffffffff中的值为 []
因此 res[0] = []
而我们需要得到的是res[0] = [a,b,c] 这样的结构。 因此我们每次在push时,需要重新生成一个数组传入,即使用path.slice()
栗子 - 数组总和II
题目描述
算法思路:
以传入参数combinationSum2([10, 1, 2, 7, 6, 1, 5], 8)
为栗子进行说明:先将传入的数组candidates进行升序排列, 即candidates = [1,1,2,5,6,7,10], 采用树的深度优先遍历.
- resides: 离target =8 , 差多少
- candicates: 当前的用于操作的候选数组
- path: 当前的路径
- res: 最终返回的结构
当resides < 0时, 直接退出当前
当resides ===0 时,代表 path中的数组满足条件. 将path推入res中 res.push(path.slice())
当resides > 0 时, 遍历candidates数组:
-
每次判断 resides - candidates[i] 是否大于0 , 若小于0则进行剪枝(退出当前循环)
-
同时要考虑[1,2, 5] 和 [1,7]的情况.因为原数组中有2个1, 只需第一个1即可.
if(candidate[i] !== candidate[i-1])
-
到这里就是正常的递归回溯工作了:
- 每次将 candidates[i]推入path中.
- 然后调用 dfs()递归
- 出来回溯. path.pop()
var combinationSum2 = function(candidates, target) {candidates.sort((a, b) => a - b)var res = []function dfs(resides, candidates, path) {if (resides < 0) returnif (resides === 0) res.push(path.slice())for (let i = 0, len = candidates.length; i < len; i++) {if (candidates[i] !== candidates[i - 1]) {if (resides - candidates[i] < 0) breakpath.push(candidates[i])dfs(resides - candidates[i], candidates.slice(i + 1), path)path.pop()}}}dfs(target, candidates, [])return res
}
栗子 - 组合总和 III
题目参考
算法思路: 还是使用深度优先.
- candidates:当前用于操作的数组
- path: 当前的路径
- resides: 当前距离目标的差值
每次进入 dfs:
-
首先检查条件是否满足:
- resides若为负数,退出当前环境
- path.length 若等于 k 则退出当前环境
- candidates存在且candidates的长度为0,则退出当前环境
-
然后循环candidates,对于每个candidates[i]
- 得到当前的path = […path, candidates[i]]
- 计算当前path长度,若等于k 则判断 resides - candidates[i] 是否为0, 若为0,则将当前路径推入res中。并退出
- 递归调用 dfs(resides - candidates[i], candidates.slice(i+1), path)
- 这里需要回溯 path.pop()
var combinationSum3 = function (k, n) {var res = []function dfs(candidates, resides, path) {if ((candidates && candidates.length == 0) || path.length > k || resides < 0) returnfor (let i = 0, len = candidates.length; i < len; i++) {path = [...path, candidates[i]]if (path.length === k && resides - candidates[i] == 0) return res.push(path.slice())dfs(candidates.slice(i + 1), resides - candidates[i], path)path.pop()}}dfs([1, 2, 3, 4, 5, 6, 7, 8, 9], n, [])return res
}
栗子 - 从根到叶子节点数字之和
题目参考
思路:
- 使用 sum 保存总和, r 保存当前树结构的根节点, path 保存当前的路径(数组类型)
- 使用dfs深度优先遍历树, 对于每次遍历 dfs(root, path)
- 判断r 是否为空,若空则返回,否则执行下一步
- 更新当前的path = […path, r.val]
- 判断当前是否为叶子节点:
- 若是则:
sum += path.join('') - 0
. 其中-0
是数字的隐式类型转换
- 若是则:
- dfs(r.left, path)
- dfs(r.rigth, path)
- 回溯 path.pop()
var sumNumbers = function (root) {var sum = 0;function dfs(r, val) {if (!r) returnval = [...val, r.val]if (!r.left && !r.right) {return sum += val.join('') - 0}dfs(r.left, val)dfs(r.right, val)val.pop()}dfs(root, '')return sum
};
栗子 - 路径总和II
题目参考
算法思路:大体的思路是深度优先遍历,遍历顺序5 -> 4 -> 11 -> 7 --> 11 -> 2 --> 11 --> 4 -->5
其中,->
代表下一个-->代表回退
。递归循环调用dfs函数.传入当前的树结构的根节点r、距离总和差值resides和当前的路径path
每次dfs循环如下:
- 判断r是否为null, 若是则返回
- 生成当前的path.
- 判断 resides - r.val 是否为0
- 若为0,则判断当前是否是叶子节点
- dfs 当前节点的左节点和右节点
var pathSum = function(root, sum){var res = []function dfs(resides, r, path){if(!r) returnpath = [...path, r.val] // 这里使用[]隐式规则,在新的内存空间中生成了一个数组if(resides - r.val === 0 && !r.left && !r.right) return res.push(path)dfs(resides - r.val, r.left, path)dfs(resides - r.val, r.right, path)}dfs(sum, root, [])return res
}