编程语言
首页 > 编程语言> > 回溯算法总结点睛

回溯算法总结点睛

作者:互联网

一、基本概念

1、什么是回溯法?

又称回溯搜索法,说白了就是一种搜索方式。

其实回溯是递归的副产品,只要有递归就会有回溯。

回溯函数也可以称之为递归函数。

2、回溯法的效率

回溯法本质是穷举,因此并不高效

可以通过一些剪枝的操作稍微提高一些效率,但仍是比较低效的算法

3、回溯法解决的问题

经常用来解决以下 5 种问题:

* 1)组合问题:**不强调**元素的顺序,N个数里按照一定的规则找出K个数的集合
* 2)排列问题:**强调**元素的顺序,N个数按一定的规则全排列,求有多少排列方式
* 3)切割问题:一个字符串按照一定规则有几种切割方式
* 4)子集问题:一个N个数的集合中有多少个符合条件的子集
* 5)棋盘问题:N皇后,解数独 等等。

4、如何理解回溯法

所有回溯法解决的问题,都可以抽象为树形结构。

因为递归法解决的都是在集合中 递归查找子集,集合的大小就构成了树的宽度,递归的深度就构成了树的深度。

递归就要有终止条件,所以必然是一棵高度有限的N叉树。

5、回溯法模板

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

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

6、剪枝:

不要想的太复杂了 ,其实嘛~ 可以剪枝的地方就是在递归中每一层的for循环所选择的起始位置,所以 如果 for 循环选择的起始位置之后的元素个数 已经不足 我们需要的元素的个数,那么就没必要搜索啦啦啦~~~,也就是控制一下 for 循环的遍历终止条件

* 已经选择的元素的个数	**path.size()**
* 还需要的元素个数          **k - path.size()**
* 在集合n中,从    **n - (k - path.size()) + 1**    位置开始 往后的遍历均为无效遍历,因此就剪掉他
* 剪枝后的最终版本    **for(int i = 1; i <= (n - (k - path.size() + 1); i++)**

7、做题感想

其实,你得心里明白,for 循环中,就是横向走,递归呢,就是纵向走,

把握

回溯函数的参数决定了你这个代码的复杂程度,参数设定的比较合理的话,整个程序就可以得到简化,

就比如 字母+数字组合 ,这道题的 下标有两个,一个是字母字符串的下标,一个是输入的数字字符串的下标,怎么有机的联合起来是需要斟酌的问题

8、时间复杂度

主要分为几种题型来总结

n 表示数组的大小

二、做题

第39题 & 第40题 组合问题

40与39的区别主要有两点:

​ 1、40题中 给定数据集中的每个数字在组合中只能使用一次,而39题可以重复使用

​ 2、40题中 给定的数据集中的元素本身有可能是重复的,而39题是无重复的

首先第一步想到的可以是把所有组合求出来,在用 set 或者 map 去重,但是呢这么做很容易超时,所以最好的是在搜索的过程中就去掉重复的组合。

这里的重复其实是有两个层面的理解,一个是同一树枝上重复,一个是同一树层上重复,

第40题,是允许同一树枝重复,但是不允许同一树层重复的结构,

去重的概念有两种,树枝的去重与树层的去重

第131题 分割回文串

这里:

横向表示在不同的位置切

纵向表示切几刀,每往下一层就表示多切一刀

终止就是切到了最后一个元素后面

图片

本题是挺难的呢~

      // 截取的过程,真的没有想到哇~边截取,边判断是否为回文串
      if(isPalindrome(s, startIndex, i))
     {
        // 获取区间内的子串是回文串,则截取这段子串放进 path 中
       string str = s.substr(startIndex, i - startIndex + 1);
       path.push_back(str);
      }

debug 报错超时的查错思路:

1、一般大规模用例的话,那就是代码写的不够精简,用时比较就

2、一般小规模用例的话,那一定就是代码有bug,一般在while循环中找错误,这种通常在while循环中会犯错

3、如果是有递归太深的话,会报错 stack overflow 栈溢出

第46题 全排列

处理排列问题是需要一个 used 数组,就不需要 startIndex ,这个used数组是用来记录是否用过这个元素

取的是叶子节点。

第47题 全排列V2

used[i-1] == true 说明同一树枝 nums[i-1] 使用过
used[i-1] == false 说明同一树层 nums[i-1] 使用过
如果同一树层 nums[i-1] 使用过,则直接跳过
这里used[i-1] == true 或者used[i-1] == false 都是可以通过的

对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!
树层上对前一位去重非常彻底,效率很高,
树枝上对前一位去重虽然最后可以得到答案,但是做了很多无用搜索

used[i-1] == false 树层去重的树形结构

图片

used[i-1] == true 树枝去重的树形结构

图片

第332题 重新安排行程

题解思路:

1、有点类似于 图论中的深度优先搜索 dfs

2、的确是有用到深搜,在查找路径的时候,也有用到回溯,因为找路径嘛,不回溯怎么找路径呢?

其实深搜一般也都是用回溯法的思路。

难点:

1、一个行程中,如果航班处理不好,有可能成为一个圈,变成死循环 (一张机票不是只能用一张嘛,用完一张弃之应该就可以了吧)

2、要求按字母排序 取得最终的结果,思考该怎么记录映射关系呢 (母鸡哦~)

​ 答:按字母排序,就可以用 std::map 或者std::set 或者 std::multimap

3、使用回溯法,那么终止条件是什么呢? (每张机票都使用一遍吗?)

4、搜索的过程中,如何遍历一个机场所对应的所有机场呢? (也是母鸡的)

​ 答:一个机场映射多个机场,可以用 map ,注意 std::unordered_map 效率相对是高的

剖析:

1、这道题是第一次用 关联容器 嵌套一个 关联容器 ,可以有两种选择,

// 但是这个有一个缺点,当使用set容器来存储时,一旦删除其中的元素,迭代器就会失效。。。为什么要删除呢?因为用过的机票不删除的话,可能会出现死循环
unordered_map<string, multiset<string>> targets		// 含义::unordered_map<出发机场,到达机场的集合>

// 使用map来存储多个机场的集合,用 “航班次数” int值 来标记是否使用过这个机场,就无需对集合做删除或者增加元素的操作
unordered_map<string, map<string, int>> targets		// 含义::unordered_map<出发机场, 航班次数>

2、通常说回溯函数的返回值类型都是 void ,这道题把递归函数设定为 bool ,

​ 因为只需要找到一条符合条件的行程即可返回,无需遍历全部,所以找到一个符合题意的,也就是一个到达叶子节点的树枝,就控制函数 return

3、终止条件分析

​ 机场个数 等于 机票个数+1,就说明找到了一个行程,把整条航班串起来了。

4、单层递归逻辑

​ 可以说本题是需要找到一个数据进行排序的容器,而且还要容易增删元素,且迭代器还不能失效

5、这道题真的不简单哦~

其实,感觉,很大程度上,难是因为对容器的操作并不是很熟练,看到这种容器的嵌套,就觉得恐惧,但是~~不要怂!冲冲冲!!!~ 先看会,再写对!

​ map容器嵌套一个map容器的用法,很妙,但是很难想得到,可太难了~

​ 不过理解了一个点,其实一个关联容器,也就是哈希映射,就把它理解为数组,对于set容器也是一样的理解,

​ 数组的下标就是 key,数组中存放的元素就是 value

​ map 的 key 根据选择的容器类型不同,可以是有序或者无序,map 的 value 就是对应着 key 的一个元素

​ set 的 key 根据选择的容器类型不同,可以是有序或者无序,

图片

这里再复习一下

图片

图片

第51题 棋盘问题–N皇后

N皇后问题是回溯算法解决的经典问题,同上一题相似,又是用到二维矩阵。

首先,先理清 皇后们 的约束条件:

​ 1、不能同行

​ 2、不能同列

​ 3、不能同斜线

把整个棋盘初始化为一个 N * N 的点点组成的矩阵,找到合法位置就将其覆盖为 Q 代表把皇后放置在那里

// 这里的初始化属于打死我也想不到的,一个一维的 vector 容器,硬生生初始化为一个二维方阵,很妙,但是我想不到哇
vector<string> chessboard = (n, string(n, '.'))


图片

第37题 解数独

​ 数独的规则:

1、定数独 永远是 9*9 的形式

2、给定的数独序列只包含数字 1-9 和 ’ . ’ ( ’ . '表示空白格 )

3、可以假设给定的数独只有唯一解

4、游戏规则: 同行同列不可重复,且每一个以粗实线分隔的3*3的宫内只能出现一次,

数独问题 需要用 二维递归 的回溯暴力搜索,之前做的题目都是一维递归,包括N皇后,因为N皇后虽然是一个矩阵,但是它每一行每一列只放一个皇后,

只需要一层 for 循环遍历一行,递归来遍历列,然后一行一列来确定皇后的唯一位置。

数独的棋盘中,每一个位置都要放置一个数字,并且检查数字是否合法,解数独的树形结构要比N皇后的更宽更深。

回溯三部曲:

标签:map,遍历,递归,元素,算法,点睛,回溯,节点
来源: https://blog.csdn.net/linping_/article/details/118077256