前端重拾算法数据结构一个月(9)

121 阅读5分钟

第九天

从头头来学习回溯算法。

什么是回溯法

回溯递归是相辅相成的,只要有递归就会有回溯。回溯通常隐藏在递归函数的下面。

使用原因及解决的问题

回溯算法是一个纯暴力的算法。它并不是一个高效算法。它解决的问题一般是要使用这种纯暴力方法才能够解决的比较复杂的问题,如下有:

  1. 组合问题:高中数学中学的排列组合其中的组合。一般是例:给一个集合[1,2,3,4],找出其中长度为2的组合。
  2. 切割问题:通常都是给一个字符串,问他有几种切割的方式。或者说再加一些特定的条件。
  3. 子集问题:也是例如给一个集合[1,2,3,4],要求出它所有的子集。
  4. 排列问题:强调元素的顺序,而组合问题不强调元素顺序。
  5. 棋盘问题:N皇后和解数读等题目。

理解回溯算法

回溯法都可以抽象为一个树形结构。我们知道回溯就是一个递归的过程,而递归都是有终止的。回溯法通常都可以抽象为一个n叉树。一般来说这棵树的宽度是回溯法中处理的集合的大小,也就是每个节点所处理的集合的大小,通常是用for循环来遍历的。这棵树的深度就是这个递归的深度,树的纵向通常就是递归来处理的。

回溯法的模板

一般来说回溯法的递归函数都是没有返回值的,就是一个void,这个递归函数一般取名叫backtracking。递归是需要终止函数的,一般来说再终止函数里就要收集结果了。树形结构中,对于一般问题,例如说:组合问题、切割问题、排列问题、部分棋盘问题都是在叶子节点去收集结果。只有子集问题是在每个节点都收集结果。处理完终止条件之后呢,就进入了一个单层搜索的逻辑。一般情况下是一个for循环,其参数通常放的是集合里的元素集,通常在这里处理节点,处理的节点是用于将放入结果集中的数据(满足条件的数据)先放入一个集合中,之后它们才能被放入结果集。接着走一个递归。递归下面就是回溯操作:实际上就是撤销处理节点的操作。

void  backtracking(参数){
    if(终止条件){
        收集结果
        return
    }
    for(集合元素){
        处理节点
        递归函数
        回溯操作
    }
    return
}

组合问题

回溯算法就是通过递归来控制有多少层for循环,递归里的每一层是一个for循环。

组合是无序的,元素也是不可重复的。每次递归的时候通过传入一个参数,来控制每次搜索的起始位置。

回溯三部曲

  1. 第一步:确定递归函数的参数以及返回值
  2. 第二步:确定终止条件
  3. 第三步:确定单层搜索(递归)的逻辑

例题:给定一个集合{1,2,3,4},找出所有的两位组合(无序、元素不重复)

第一步: 确定参数和返回值:可以把一个答案组合例如[1,2]看成一个一维数组,我们把它取名为path。返回值是由所有path组成的数组,即为一个二维数组,我们把它取名为result。在递归中呢需要有限制其范围的n即表示给定的集合大小的值,在这个题目中就是4。还有k来表示组合的大小,该题目中就是2了(两位组合)。还需要一个startIndex用来标志每次搜索的起始位置。目前得到的代码如下:

const path:<number[]> = []
const result:<number[][]> = []
const backtracking = (n,k,startIndex)=>{}

第二步: 确定终止条件:在该题目中,我们的每次搜索,当搜索到两个数字的时候,即组合长度为2的时候,就满足了题目的条件即k=2,这个时候就应该返回了。所以我们的终止条件就是当path.length===2的时候。目前得到的代码如下:

const path:<number[ ]> = [ ]             //一维数组
const result:<number[ ][ ]> = [ ]         //二维数组
const backtracking = (n,k,startIndex)=>{
    if(path.length===2){                      //当path的长度为2的时候,收集结果
        result.push(path)
        return                                        //也可以直接return result.push(path)
    }
}

第三步: 确定单层搜索逻辑:在该题中,是在每个for循环中,从startIndex(起始位置)开始,去遍历剩余的元素。

const path:<number[ ]> = [ ]             
const result:<number[ ][ ]> = [ ]         
const backtracking = (n,k,startIndex)=>{
    if(path.length===2){                     
        result.push(path)
        return                                        
    }
    for(i=startIndex;i<=n;i++){
        path.push(i)                 //把元素i推入path中
        backtracking(n,k,i+1)  //递归(调用自己),这里startIndex需要从i的下一位开始,才不会重复
        path.pop()                   //回溯,例如[1,2]满足条件被存入result后,将2弹出,又回到[1],
                                           然后再将3放进来组成[1,3],以此类推。
    }
}

backtracking(4,2,1)        //执行回溯算法,最终就结果会被存到全局声明的result中

这样就是一个组合问题的基本解法,明天学习组合问题的剪枝操作等等。