回溯算法解析

本文详细介绍了回溯法的概念、算法思想及实现方式,包括递归和非递归两种实现。回溯法适用于解决复杂问题,通过深度优先搜索遍历问题空间,遇到无效解时进行回溯。文章还提到了动态规划和DFS的对比,并强调了回溯法在路径、选择列表和结束条件上的关键要素。同时,提供了回溯法的剪枝策略和在编程解题中的应用指导,包括如何构造递归结构、确定递归出口、明确所有路径以及如何回溯。
摘要由CSDN通过智能技术生成

1 回溯法定义

采用试错的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。
注:适用于复杂和规模较大的问题且多个步骤组成的问题,并且每个步骤都有多个选项,当我们在某一步选择了其中一个选项时,就进入下一步,然后又面临新的选项。我们就这样重复选择着,直至到达最后的状态。一般先画树状图表示。

2 算法思想

从根节点按照深度优先搜索的方法进行遍历,当搜索到某一结点,判断是否包含问题的解:
• 包含:继续搜索
• 不包含:逐层回溯父节点。
结束条件
直到找到一个可能存在的正确的答案,若没有则宣告该问题没有答案。
PS: 深度优先搜索 算法(英语:Depth-First-Search,DFS)是一种用于遍历或搜索树或图的算法。这个算法会 尽可能深 的搜索树的分支。当结点 v 的所在边都己被探寻过,搜索将 回溯 到发现结点 v 的那条边的起始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个进程反复进行直到所有结点都被访问为止。

3 算法实现

1、路径:已经做出的选择。
2、选择列表:当前可以做的选择。
3、结束条件:到达决策树底层,无法再做选择的条件。

3.1 递归回溯

result = []
def backtrack(路径, 选择列表):
  if 满足结束条件:
    result.add(路径)//存放结果
    return
  for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
    处理节点;
    backtrack(路径,选择列表); // 递归
    回溯,撤销处理结果//pop
  }
}

其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择

3.2 非递归回溯

设问题的解是一个n维向量(a1,a2,…,an),约束条件是ai(i=1,2,3,…,n)之间满足某种条件,记为 f(ai), 有两种可能:找到一个可能存在的正确的答案;在尝试了所有可能的分步方法后宣告该问题没有答案。

int a[n],i; //a[n]为解空间,i为深度
初始化数组 a[]; 
i = 1; 
while (i>0(有路可走) and (未达到目标)) { //还未回溯到头
    if(i > n) { //搜索到叶结点 
        搜索到一个解,输出; 
    } else {    //处理第 i 个元素 
        a[i]第一个可能的值; 
        while(a[i]在不满足约束条件且在搜索空间内) {
            a[i]下一个可能的值; 
        }//while 
        if(a[i]在搜索空间内) {
            标识占用的资源; 
            i = i+1; //扩展下一个结点 
        } else { 
            清理所占的状态空间; //回溯 
            i = i – 1; 
        }//else 
    }//else 
}//while

4 回溯策略

1.画图,观察元素是否有重复,如有重复则需要剪枝,思考如何剪枝
2.回溯三要素:路径、选择列表、结束条件
3. 按照此代码模板,写出代码。
注意:在做选择后还要撤销选择,如果做出的选择写在函数列表里如backtrack(path + [num[i]], num[:i] + num[i+1:]),那么无需撤销操作,如果写在外面path.append(nums[i]),这样就需要撤销上一步的操作。

4.0 编写检测函数(非必须)

检测此路径是否满足条件,是否通过。

4.1 明确函数功能

明确函数的输入输出以及根据题目设置函数功能。
输入、全局变量和方法参数:(当前节点状态)这个方法的参数最好由递归时的当前阶段的状态决定,并记录当前状态。----这个当前阶段的状态一般指递归深度depth,而不是第几个分支i,分支选择是由 for (int i = 0; i < length; i++) 中的 i 来决定的,不需要我们写入函数参数中。如果我们这个方法需要实现阶乘,那么我们的方法参数需要记录当前阶乘的数字(即 当前阶段的状态)。

返回数据:返回数据前一步递归的信息数据----递归函数的返回值最好设置为单个元素,比如说一个节点或者一个数值,告诉前一步递归现在的结果数据即可。
如果返回值是数组的话,将无法从中提取到任何有效信息来进行操作;
如果结果需要数组的话,将数组作为公共变量返回值为void,我们在方法体里面操作数组即可。-----因为回溯法我们一般只关心叶子结点的结果,中间的过程函数一般没什么返回的作用,所以函数返回值类型一般为void或者bool。

操作:方法参数 depth,递归出口if (depth >= n)
注意:递归深度depth,根节点深度为 0,一般根节点代表的解都为空解,所以深度为 0,之后的解深度为 1 才代表我们的第 1 次分支路径选择。

4.2 寻找递归出口

一般为某深度,或叶子节点,或非叶子节点(包括根节点)、所有节点等。决定递归出去时要执行的操作。
注:每次提交数组的集合(即 List<List<>>)的时候,都要记得创建一个新的数组来存放结果数组元素(即 new List<>(list)),不然后面操作的都是加入集合后的那个数组。

4.3 明确所有路径

这个构思路径最好用树形图表示。而分支路径的选择由 for (int i; i < length; i++)中的 i 来决定的,list.add(i),即 选择第 i 条分支路径。
例如:走迷宫有上下左右四个方向,也就是说我们站在一个点处有四种选择,我们可以画成无限向下延伸的四叉树。直到向下延伸到叶子节点,那里便是出口;从根节点到叶子节点沿途所经过的节点就是我们满足题目条件的选择。

操作:f(depth)
递归深度:方法参数 depth,递归出口if (depth >= n),递归深度增加f(depth + 1)
注意:递归深度depth,根节点深度为 0,一般根节点代表的解都为空解,所以深度为 0,之后的解深度为 1 才代表我们的第 1 次分支路径选择。
所有路径:for (int i; i < length; i++)即 枚举 i 所有可能的路径
路径分支选择:非方法参数,选择 for 循环中的分支路径 i,如 list.add(i);

注意:在选择完路径分支之后,一般都伴随着 递归深度 + 1,f(depth + 1),即 在选择完路径分支之后,我们从根节点向下移了一个深度。
在这里插入图片描述
做题的时候,建议 先画树形图 ,画图能帮助我们想清楚递归结构,想清楚如何剪枝。

分支如何产生;
题目需要的解在哪里?是在叶子结点、还是在非叶子结点、还是在从跟结点到叶子结点的路径?
哪些搜索会产生不需要的解的?例如:产生重复是什么原因,如果在浅层就知道这个分支不能产生需要的结果,应该提前剪枝,剪枝的条件是什么,代码怎么写?

4.4. 回溯

将当前栈帧弹出,返回到父节点。

4.5 剪枝方法

全排列里面的剪枝,只需要考虑后面的元素和第一个元素相同的情况,则continue。但是要注意是要保证同层不同和上下层相同(i > start).
对于全排列题不适合用mark,mark用在矩阵中元素只访问一次的情况下如访问矩阵里的坐标或者字母题。
注意事项
1.如果是选择列表循环内需要判断,用continue(表示这步不行还有机会进行下一步),如果是在选择列表循环外判断则用return中断
2.首先对当前路径进行判断,再进入下一个选择列表中。这样可以保证所有的状态都得到判断。

5 算法比较

算法特点
回溯法强调了「深度优先遍历」思想的用途,用一个 不断变化 的变量,在尝试各种可能的过程中,搜索需要的结果。强调了 回退 操作对于搜索的合理性 ;一般使用在问题可以树形化表示时的场景
DFS强调一种遍历的思想
动态规划DP评估最优值是多少,最优值对应的具体解是什么并不要求。因此很适合应用于评估一个方案的效果
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值