回溯本质其实就是穷举,就是一种暴力解,回溯的复杂度会比较高。
回溯解决的问题
组合问题:N个数里面按一定规则找出k个数的集合
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有符合条件的子集
排列问题:N个数按一定规则全排列,有几种排列方式
棋盘问题:N皇后,解数独
基本模板
function backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
例题
组合问题
解题思路:
- 是一个典型的组合的
DFS,但是因为是组合问题,我们需要考虑,不可以重复。 - 所以我们需要额外设置一个
start用来表示当前是第几个元素 - 按照模板,我们需要一个
track数组,用于记录,我们走过的路,其实也就是我们选择了那些数字 - 需要一个
res,用于返回结果 - 这里我们的终止条件就是,当我们已经找到了那个组合,也就是
track的长度 ===k - 那么我们循环的条件就是一直去遍历那
n个数字,但是不可以有重复
代码:
function combine(n: number, k: number): number[][] {
// DFS
// 设置返回的结果
const res:number[][] = []
// 设置走过的路径
const track:number[] = []
backTrack(track,n,k,res,1)
return res
};
const backTrack = (track:number[],n:number,k:number,res:number[][],start:number)=>{
// 为什么要设置start,因为我们的组合不讲究顺序,所以避免重复
// 回溯条件
if(track.length === k){
res.push(Array.from(track))
return;
}
// 进行遍历
for(let i = start;i<=n;i++){
// 为什么这边不用像全排列那样进行判断
// 因为我已经规定了start,也就是,我等会所有的第一个值,一定都是之前未出现过的
track.push(i)
backTrack(track,n,k,res,i+1)
track.pop()
}
}
我们可以将组合问题看成是特定长度的子集问题,也就是回溯的结束条件,为了,track.length === nums.length
子集问题
解题思路:
- 因为这是一个子集问题,我们同样的也可以采用回溯方法
- 因为是子集问题,他其实需要的是每一个节点,所以我们可以直接将
trackpush进res里 - 因为子集,不可以产生重复,所以我们假定有一个
startIndex,规定了,不允许反向前面去遍历
代码
function subsets(nums: number[]): number[][] {
// 使用DFS
// 设置一个track表示总过的路
let track = new Array<number>()
// 设置res,表示最后的结果
let res:number[][] = new Array()
// 调用回溯
backTrack(track,nums,0,res)
return res
};
const backTrack = (track:number[],nums:number[],startIndex:number,res:number[][])=>{
// 因为是子集问题,其实就是将里面所有的节点,全部到push到结果里面
res.push(Array.from(track))
// 条件
for(let i = startIndex;i< nums.length;i++){
// push进去
track.push(nums[i])
backTrack(track,nums,i+1,res)
track.pop()
}
}
排列问题
解题思路:
- 这也是一个回溯问题,所以可以直接使用模板
- 这时候我们的条件就是,只有当
track.length===nums.length时,才会是他的全排列 - 因为是全排列,里面的
track的值是不可以相同的,所有有了track。indexOf(nums[i]) - 那么你可能会说,为啥我们不使用
find,因为0 === 0是true,那么其实他是不满足题意的,所以避免在这里使用find
代码:
function permute(nums: number[]): number[][] {
// 同样的也是使用回溯
// 设置一个res,用于承接结果
let res:number[][] = new Array()
// 设置track,表示,当前走过的路径
let track = new Array<number>()
backTrack(res,track,nums)
return res
};
const backTrack = (res:number[][],track:number[],number:number[])=>{
//条件
if(track.length === number.length){
res.push(Array.from(track))
return ;
}
// 进行递归
for(let i = 0;i<number.length;i++){
// track.push(number[i])
// 避免出现重复
// 重复出现,那么直接跳过
if(track.indexOf(number[i]) !== -1){
// track.push(number[i])
continue
}
track.push(number[i])
backTrack(res,track,number)
track.pop()
}
}
电话号码的字母组合
链接:17. 电话号码的字母组合 - 力扣(LeetCode)
解题思路:
-
其实这也是一种组合问题,所以显然也是使用回溯做
- 这就意味着我们需要一个
Map类似的东西,来存储对应的数据 - 并且因为是组合问题,不可避免地我们需要
index,这里的index,就是为了不重复 - 条件是,我们的找到了组合
track.length === digit.length - 但是我们需要便利的是数字对应的字母
代码:
function letterCombinations(digits: string): string[] {
if (digits === '') return [];
// 首先想要将这个字母与数字的对相应写出来
// 设置track
let digit:string[] = digits.split("")
let track = new Array()
// 承接结果
let res = new Array<string>()
const letterMap: { [index: string]: string[] } = {
0:[],
1: [],
2: ['a', 'b', 'c'],
3: ['d', 'e', 'f'],
4: ['g', 'h', 'i'],
5: ['j', 'k', 'l'],
6: ['m', 'n', 'o'],
7: ['p', 'q', 'r', 's'],
8: ['t', 'u', 'v'],
9: ['w', 'x', 'y', 'z'],
}
// 这里面其实就是一种组合,所以使用回溯
backTrack(digit,track,res,0,letterMap)
return res
};
const backTrack = (digit:string[],track:string[],res:string[],index:number,letterMap:{[index:string]:string[]})=>{
// 条件
if(track.length === digit.length){
res.push(track.join(""))
return ;
}
// 当前的字母
let letter = letterMap[digit[index]]
for(let i = 0;i<letter.length;i++){
track.push(letter[i])
backTrack(digit,track,res,index+1,letterMap)
track.pop()
}
}
参考链接: