前言
在正式接触回溯算法之前,我们先来看一道题目leetcode.77组合 。
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
看到这个题目,首先我们可以想到利用for循环不断遍历来获取所有组合,但是题目中的k值并不是一直为2。如果是3或者更大的数呢,很显然这种方法是行不通的。这里就需要用到我们接下来要说的回溯算法。
那么接下来我们就以leetcode上的这道题目为例来讲一下什么是回溯算法。
一、什么是回溯算法?
回溯是递归的副产品,只要有递归就会有回溯,所以回溯法也经常和二叉树遍历,深度优先搜索混在一起,因为这两种方式都是用了递归。
回溯法就是暴力搜索,并不是什么高效的算法,最多再剪枝一下。
回溯算法能解决如下问题:
组合问题:N个数里面按一定规则找出k个数的集合
排列问题:N个数按一定规则全排列,有几种排列方式
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
棋盘问题:N皇后,解数独等等
二、回溯的模板
上文我们提到了回溯是递归的产物,那么我们再说一下递归的三部曲:
1.确定函数的参数和返回值
2.确定终止条件
3.确定单层处理逻辑(每层递归所需要处理的信息)
根据上面的步骤我们可以得出以下代码
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
三、组合问题
由于回溯算法本身不易理解,所以我们以leetcode77.组合这道题目来进一步分析一下回溯算法的过程。
前面我们提到了二叉树遍历,所以为了易于理解我们把问题转化为树型结构。
1. 在四个数中取出一个数。
2. 在剩余三个数中取一个数得到一个我们想要的结果集合,回溯到上一个节点再取另一个数,直到取完为止。
3. 每次取完就回溯到上一个节点,直到取完所有节点。
从上述步骤我们可以看出,整个完整的过程其实就是DFS遍历N叉树的过程。
我们可以得出以下代码(golang版本)
var res [][]int
var path []int
func combine(n int, k int) [][]int {
res =res[:0]
path=path[:0]
dfs(n,k,1)
return res
}
func dfs (n,k,startIndex int){
if len(path)==k{//取到足够的数,向结果集合中添加
res = append(res,append([]int{},path...))
return
}
for i:=startIndex;i<=n;i++{
path = append(path,i)//向path中加入本层取的数
dfs(n,k,i+1)
path = path[:len(path)-1]//返回上一层并删除最后一个数
}
}
在这个过程中我们是遍历了所有的节点,但是从图中我们可以很明显的看出来,有一部分节点是不满足题意的,所以我们可以对回溯进行一个剪枝优化。那么怎么剪枝呢?
剪枝优化
根据组合这道题,我们可以得出两种数:我们所需要取的数
以及我们还能取的数
。
我们只需要保证我们所需要取的数
比我们还能取的数
少即可。
代码如下:
var res [][]int
var path []int
func combine(n int, k int) [][]int {
res =res[:0]
path=path[:0]
dfs(n,k,1)
return res
}
func dfs (n,k,startIndex int){
if len(path)==k{//取到足够的数,向结果集合中添加
res = append(res,append([]int{},path...))
return
}
//k-len(path) 还需要取的数
//n-startIndex+1 剩余的数
if n-startIndex+1<k-len(path){
return
}
for i:=startIndex;i<=n;i++{
path = append(path,i)//向path中加入本层取的数
dfs(n,k,i+1)
path = path[:len(path)-1]//返回上一层并删除最后一个数
}
}
四、组合总和
题目来源:leetcode39.组合总和
组合总和是在组合问题上的一种延申,他们的整体思路是一样的,只是各自的终止条件以及剪枝操作不同。
我们先看一下树型结构。
我们要注意的是题目中要求可以无限重复使用一个数,所以我们在向下遍历即递归过程中我们传递的起始位置不再加一,这样就可以遍历同一个数。
从图中我们可以看出,sum
并不一定等同于target
,所以我们的终止条件便是sum==target
,之后的剪枝操作便很容易想到sum>tartget
。
代码实现:
var res [][]int
var path []int
func combinationSum(candidates []int, target int) [][]int {
res = res[:0]
path = path[:0]
dfs(candidates,0,0,target)
return res
}
func dfs (candidates []int,start,sum,target int){
if sum ==target{
tmp :=make([]int,len(path))
copy(tmp,path)
res = append(res,tmp)
return
}
if sum>target{
return
}
for i:=start;i<len(candidates);i++{
path = append(path,candidates[i])
dfs(candidates,i,sum+candidates[i],target)
path = path[:len(path)-1]
}
}
五、组合总和II
题目来源:组合总和II
组合总和II是组合总和问题的一种延申,其实就是在集合中会出现重复的数。
我们先来看一下转换之后的树型结构。
我们可以看到虽然我们遍历出了和为3的所有组合,但是其中存在大量的重复组合。
所以我们的剪枝操作不光要判断和是否大于3,还要保证在每一层中不会遍历重复的数。
我们可以对原数组进行排序,并在每一层中都判断此次所要取的数是否与上一次所取的数相等,即在横向遍历(for
循环)中加入if (nums[i]==nums[i-1){continue}
。
注意:要保证i
与i-1
都是在本层,即(i-1>=start===>i>start
)
接下来我们看一下代码实现
var res[][]int
func combinationSum2(candidates []int, target int) [][]int {
res = res[:0]
sort.Ints(candidates)
dfs(candidates,[]int{},0,0,target)
return res
}
func dfs (candidates,path []int,sum,start,target int){
if sum>target{
return
}
if sum ==target{
res = append(res, append([]int{},path...))
return
}
for i:=start;i<len(candidates);i++{
if i>start&&candidates[i]==candidates[i-1]{ //在本层中i与i-1位置的数重复,跳过i
continue
}
path = append(path,candidates[i])
dfs(candidates,path,sum+candidates[i],i+1,target)
path = path[:len(path)-1]
}
}
六、全排列
题目来源:leetcode46.全排列
老样子我们先看看转化之后的树形结构
前面我们讲的组合问题是在树形结构中的同一层中,如果某一层取了一个数,那么其它层便不会取这个数,所以我们设置了一个起始位置start
,然后再排列中我们每个元素都需要取到,所以我们便不需要设置起始位置了,每次都在0
开始遍历。但是我们仍要注意不要遍历同一个位置的元素,所以我们使用了used
来对每一个位置进行标记。
接下来看代码
var res [][]int
var path []int
func permute(nums []int) [][]int {
res = res[:0]
path = path[:0]
used :=make([]bool,len(nums))
dfs(len(nums), nums,used)
return res
}
func dfs(n int, nums []int,used []bool) {
if len(path) == n {
tmp := make([]int, len(path))
copy(tmp, path)
res = append(res, tmp)
}
for i := 0; i < n; i++ {
if used[i]{ //在这一层已经遍历过i的,跳过
continue
}
path = append(path, nums[i])
used[i] = true
dfs(len(nums), nums,used)
used[i] = false
path = path[:len(path)-1]
}
}