[算法整理]回溯

130 阅读3分钟

回溯本质其实就是穷举,就是一种暴力解,回溯的复杂度会比较高。

回溯解决的问题

组合问题:N个数里面按一定规则找出k个数的集合

切割问题:一个字符串按一定规则有几种切割方式

子集问题:一个N个数的集合里有符合条件的子集

排列问题:N个数按一定规则全排列,有几种排列方式

棋盘问题:N皇后,解数独

基本模板

function backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

例题

组合问题

链接:77. 组合 - 力扣(LeetCode)

解题思路:

  • 是一个典型的组合的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

子集问题

链接:78. 子集 - 力扣(LeetCode)

解题思路:

  1. 因为这是一个子集问题,我们同样的也可以采用回溯方法
  2. 因为是子集问题,他其实需要的是每一个节点,所以我们可以直接将trackpush进res
  3. 因为子集,不可以产生重复,所以我们假定有一个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()
    }
}

排列问题

链接:46. 全排列 - 力扣(LeetCode)

解题思路:

  1. 这也是一个回溯问题,所以可以直接使用模板
  2. 这时候我们的条件就是,只有当track.length===nums.length时,才会是他的全排列
  3. 因为是全排列,里面的track的值是不可以相同的,所有有了track。indexOf(nums[i])
  4. 那么你可能会说,为啥我们不使用find,因为0 === 0true,那么其实他是不满足题意的,所以避免在这里使用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)

解题思路:

  1. 其实这也是一种组合问题,所以显然也是使用回溯做

电话组合题.png

  1. 这就意味着我们需要一个Map类似的东西,来存储对应的数据
  2. 并且因为是组合问题,不可避免地我们需要index,这里的index,就是为了不重复
  3. 条件是,我们的找到了组合track.length === digit.length
  4. 但是我们需要便利的是数字对应的字母

代码:

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()
    }
}

参考链接:

代码随想录 (programmercarl.com)

回溯算法解题套路框架 :: labuladong的算法小抄 (gitee.io)

回溯算法秒杀所有排列/组合/子集问题 :: labuladong的算法小抄 (gitee.io)