第十天打卡——回溯

本文详细阐述了回溯法的基本原理、应用问题类型(如组合、排列、组合总和等),以及如何通过剪枝优化提高效率。重点讲解了回溯算法的模板、组合问题中的剪枝策略,以及电话号码字母组合和IP地址复原等问题的解决方法。
摘要由CSDN通过智能技术生成

回溯法其实就是暴力查找,想效率高点就配上剪枝;回溯法解决的问题都可以抽象成树形结构。 

回溯法,一般可以解决如下几种问题:

  • 组合问题:N个数里面按一定规则找出k个数的集合
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 排列问题:N个数按一定规则全排列,有几种排列方式
  • 棋盘问题:N皇后,解数独等等

另外,组合是不强调元素顺序的,排列是强调元素顺序

例如:{1, 2} 和 {2, 1} 在组合上,就是一个集合,因为不强调顺序,而要是排列的话,{1, 2} 和 {2, 1} 就是两个集合了。组合无序,排列有序

回溯三部曲:

  • 回溯函数模板返回值以及参数
  • 回溯函数终止条件
  • 回溯搜索的遍历过程

回溯算法模板框架如下:

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

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

77. 组合  

组合问题:要理解横向遍历——for循环纵向遍历——递归

组合问题不同于全排列,需要去掉一些重复的组合,此时需要用到startIndex指针来表示,每一次集合收缩后的起始值

将问题抽象成树形结构 

每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围

图中可以发现n相当于树的宽度,k相当于树的深度

图中每次搜索到了叶子节点,我们就找到了一个结果。相当于只需要把达到叶子节点的结果收集起来,就可以求得 n个数中k个数的组合集合。

开始回溯:

  • 明确返回值和参数:

设置两个全局变量保存单次结果,和结果集;故无返回值。参数:n,k,startIndex

  • 明确终止条件:

叶子节点:单次结果的大小==k的大小,保存此时的结果到结果集

  • 明确单次回溯逻辑:

for循环进行横向遍历,循环中嵌套递归进行纵向遍历,递归结束后回溯

剪枝优化:

对于每一次的for循环,如果收缩后的集合大小比k小,则不应该继续递归下去,进行剪枝。

接下来看一下优化过程如下:

  1. 已经选择的元素个数:path.size();

  2. 还需要的元素个数为: k - path.size();

  3. 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历

例如:n = 4,k = 3

目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。

截止到从2开始搜索都是合理的,可以是组合[2, 3, 4]。对应第二层

目前已经选取的元素为1(path.size为1),n - (k - 1) + 1 即 4 - ( 3 - 1) + 1 = 3。

截止到从3开始的搜索都是合理的,可以是组合[1, 3, 4]或[2, 3, 4]。 对应第三层

目前已经选取的元素为2(path.size为2),n - (k - 2) + 1 即 4 - ( 3 - 2) + 1 = 4。

截止到从4开始的搜索都是合理的,可以是组合[1, 2, 4]或[1, 3, 4]或是[2, 3, 4]。 对应第四层

 但是再往后的for循环就没必要了


216.组合总和III

是77.组合题的变型,比较常规。

需要注意的地方:有两处剪枝操作。


17.电话号码的字母组合

思路不难想。问题在于字符串下标的获取,数值与字符串的对应

需要注意的有三点:

1、java字符串相关的函数方法不熟悉,总是出现细节上的问题。

2、回溯中的横向循环和纵向递归的起始条件

      通过设置index指针来遍历digits中的单个字符(纵向递归)

      for循环起始值与之前题目不同,起始值为0,每次for循环的字符串不同,不需要考虑重复问题。有点类似于全排列。(横向循环

 3、回溯的终止条件


39. 组合总和

思路不难想,自己想出来代码也能一刷写出来,问题在于剪枝优化。

重点看一下如何剪枝优化

先对总集合排序。

Arrays.sort(candidates); // 先进行排序

排序后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历

for (int i = idx; i < candidates.length; i++) {
            // 如果 sum + candidates[i] > target 就终止遍历
            if (sum + candidates[i] > target) break;
            path.add(candidates[i]);
            backtracking(res, path, candidates, target, sum + candidates[i], i);
            path.remove(path.size() - 1); // 回溯,移除路径 path 最后一个元素
        }

40.组合总和II

与上一题的区别是:

1、本题candidates中的每个数字在每个组合中只能使用一次。

2、本题数组candidates的元素是有重复的,而39.组合总和 (opens new window)是无重复元素的数组candidates

集合(数组candidates)有重复元素,但结果不能有重复的组合。 

所以要去重。去重,就是使用过的元素不能重复选取

组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的。

一个维度是同一树枝上使用过,一个维度是同一树层上使用过。

没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因。

题目中元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。

所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重

 代码实现:

没有使用used数组,先排序后根据startIndex指针实现了for循环去重。


分割

切割问题类似组合问题

例如对于字符串abcdef:

  • 组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个.....。
  • 切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段.....。

分割问题同样可以抽象成一个树形结构:

131.分割回文串

优化:没看。优化放在了如何判断回文字符串上。


93.复原IP地址 

思路有些复杂。

1、明确参数和返回值

参数:字符串,startIndex,pointNum(记录逗号的个数) 无返回值

2、明确终止条件

首先pointNum==3

然后判断剩余部分是否合法。如果合法,将结果存入结果集;最后return

3、明确单次回溯逻辑

经典的 for (int i = startIndex; i < s.size(); i++)   

字符串截取[startIndex,i]的字符,判断是否合法

如果合法,添加逗号且pointNum+1,递归。回溯,删去逗号且pointNum-1。

递归时:startIndex+2,因为有逗号的存在

如果不合法,break跳出循环(这里需要注意一下,为什么不是continue)

判断是否合法有4种情况:

1、区间不合法 2、不能有非数字 3、开头不能是0但单独一个数可以为0 4、不能大于225

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值