一、理论基础
回溯法也叫回溯搜索法。回溯法的本质是穷举,穷举所有可能,从中找到想要的答案。回溯法效率其实并不高,但可以通过剪枝操作适当地提高一些效率。
因为部分问题即使暴力搜索也很难搜索出来,所以需要用到回溯法。例如今天的组合问题,N个数里面按一定规则找出k个数的集合,如果直接用for循环,就要用到N重for循环,直接写肯定是不行的,就需要用到递归回溯。
回溯是递归的副产品,只要有递归就会有回溯。
二、力扣题77. 组合
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
示例 1:
输入:n = 4, k = 2 输出: [ [2,4], [3,4], [2,3], [1,2], [1,3], [1,4], ]
示例 2:
输入:n = 1, k = 1 输出:[[1]]
提示:
1 <= n <= 20
1 <= k <= n
一般解决此类问题,都会先把问题抽象为树形结构(即n叉树),用这种结构来理解回溯的过程会简单很多。
因为时间比较紧,不想自己作图,引用代码随想录卡哥的树形图:
第一层按顺序取 [ 1, 4 ]中的数延伸一条枝干,再在第二层依次各取剩下的数的其中一个数延伸一条枝干,依照组合的特性,每次取数时只能取比当前取得的数更大的数。
此时需要添加两个变量。一个是数组 path:用于记录根节点到叶子节点取得的值。另一个是变量 cur:用于记录当前按顺序遍历到哪个数。
而回溯的作用正是用于使数组 path 记录根节点到叶子节点的每一条路径。
例如:path数组在第一层取 1 放入数组后,进入下一层从 2,3,4 中取得 2 放入数组,此时已到达叶子节点,把 path 数组存入结果集。然后回退到第二层,此时再把 2 从 path数组中取出来,重新从 3,4 中取得 3 放入数组。
即每次回退到上一层时,都要把 path 数组的最后一个元素取出丢弃,然后重新选取新的元素放入path 数组,这样才能保证回溯效果。
代码如下,在回溯函数中需要添加 是否是叶子节点的判断,是的话把 path 存入全局变量数组result中 。
还添加了剪枝操作,即如果当前已选的值和接下来可选的值的个数加起来 < k,则这条路径绝对不可能得到符合条件的数组,可以直接中断。例如上面的 n = 4, k = 2,当第一次取 4 时,接下来已经没有可选的值了,所以肯定达不到数组个数为 k 的要求,就可以直接中断。
class Solution {
private $result = [];
/**
* @param Integer $n
* @param Integer $k
* @return Integer[][]
*/
function combine($n, $k) {
$path = [];
$this->backtracking($n, $k, $path, 1);
return $this->result;
}
//$path 记录遍历的组合,$cur 记录当前遍历到的值
function backtracking($n, $k, $path, $cur) {
if(count($path) == $k) { //判断叶子节点
$this->result[] = $path;
return;
}
if($n - $cur + 1 + count($path) < $k) { //剪枝操作
return;
}
for($i = $cur; $i <= $n; $i++) {
$path[] = $i;
$this->backtracking($n, $k, $path, $i + 1);
array_pop($path); //回溯操作,将path的最后一个元素删除
}
}
}