更新图床

This commit is contained in:
programmercarl
2023-03-10 14:02:32 +08:00
parent 2a9b627a90
commit 17cb4b45c7
134 changed files with 1169 additions and 829 deletions

View File

@@ -1,12 +1,14 @@
<p align="center">
<a href="https://programmercarl.com/other/xunlianying.html" target="_blank">
<img src="../pics/训练营.png" width="1000"/>
</a>
<p align="center"><strong><a href="https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A">参与本项目</a>,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!</strong></p>
> 20张树形结构图、14道精选回溯题目21篇回溯法精讲文章由浅入深一气呵成这是全网最强回溯算法总结
# 回溯法理论基础
# 回溯法理论基础
转眼间[「代码随想录」](https://img-blog.csdnimg.cn/20200815195519696.png)里已经分享连续讲解了21天的回溯算法是时候做一个大总结了本篇高能需要花费很大的精力来看
@@ -49,9 +51,9 @@ void backtracking(参数) {
**事实证明这个模板会伴随整个回溯法系列!**
# 组合问题
# 组合问题
## 组合问题
## 组合问题
在[回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)中,我们开始用回溯法解决第一道题目:组合问题。
@@ -61,17 +63,17 @@ void backtracking(参数) {
本题我把回溯问题抽象为树形结构,如题:
![77.组合1](https://img-blog.csdnimg.cn/20201118152928844.png)
![77.组合1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201118152928844.png)
可以直观的看出其搜索的过程:**for循环横向遍历递归纵向遍历回溯不断调整结果集**,这个理念贯穿整个回溯法系列,也是我做了很多回溯的题目,不断摸索其规律才总结出来的。
对于回溯法的整体框架,网上搜的文章这块都说不清楚,按照天上掉下来的代码对着讲解,不知道究竟是怎么来的,也不知道为什么要这么写。
对于回溯法的整体框架,网上搜的文章这块都说不清楚,按照天上掉下来的代码对着讲解,不知道究竟是怎么来的,也不知道为什么要这么写。
**所以,录友们刚开始学回溯法,起跑姿势就很标准了!**
优化回溯算法只有剪枝一种方法,在[回溯算法:组合问题再剪剪枝](https://programmercarl.com/0077.组合优化.html)中把回溯法代码做了剪枝优化,树形结构如图:
![77.组合4](https://img-blog.csdnimg.cn/20201118153133458.png)
![77.组合4](https://code-thinking-1253855093.file.myqcloud.com/pics/20201118153133458.png)
大家可以一目了然剪的究竟是哪里。
@@ -87,11 +89,11 @@ void backtracking(参数) {
在[回溯算法:求组合总和!](https://programmercarl.com/0216.组合总和III.html)中,相当于 [回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)加了一个元素总和的限制。
树形结构如图:
![216.组合总和III](https://img-blog.csdnimg.cn/20201118201921245.png)
![216.组合总和III](https://code-thinking-1253855093.file.myqcloud.com/pics/20201118201921245.png)
整体思路还是一样的,本题的剪枝会好想一些,即:**已选元素总和如果已经大于n题中要求的和那么往后遍历就没有意义了直接剪掉**,如图:
![216.组合总和III1](https://img-blog.csdnimg.cn/20201118202038240.png)
![216.组合总和III1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201118202038240.png)
在本题中,依然还可以有一个剪枝,就是[回溯算法:组合问题再剪剪枝](https://programmercarl.com/0077.组合优化.html)中提到的对for循环选择的起始范围的剪枝。
@@ -113,7 +115,7 @@ void backtracking(参数) {
树形结构如下:
![39.组合总和](https://img-blog.csdnimg.cn/20201118152521990.png)
![39.组合总和](https://code-thinking-1253855093.file.myqcloud.com/pics/20201118152521990.png)
最后还给出了本题的剪枝优化,如下:
@@ -123,7 +125,7 @@ for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target;
优化后树形结构如下:
![39.组合总和1](https://img-blog.csdnimg.cn/20201118202115929.png)
![39.组合总和1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201118202115929.png)
### 组合总和(三)
@@ -138,7 +140,7 @@ for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target;
都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上“使用过”,一个维度是同一树层上“使用过”。**没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因**。
![40.组合总和II1](https://img-blog.csdnimg.cn/2020111820220675.png)
![40.组合总和II1](https://code-thinking-1253855093.file.myqcloud.com/pics/2020111820220675.png)
我在图中将used的变化用橘黄色标注上**可以看出在candidates[i] == candidates[i - 1]相同的情况下:**
@@ -159,7 +161,7 @@ for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target;
树形结构如下:
![17. 电话号码的字母组合](https://img-blog.csdnimg.cn/20201118202335724.png)
![17. 电话号码的字母组合](https://code-thinking-1253855093.file.myqcloud.com/pics/20201118202335724.png)
如果大家在现场面试的时候一定要注意各种输入异常的情况例如本题输入1 * #按键
@@ -187,10 +189,10 @@ for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target;
树形结构如下:
![131.分割回文串](https://img-blog.csdnimg.cn/20201118202448642.png)
![131.分割回文串](https://code-thinking-1253855093.file.myqcloud.com/pics/20201118202448642.png)
# 子集问题
# 子集问题
## 子集问题(一)
@@ -198,7 +200,7 @@ for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target;
如图:
![78.子集](https://img-blog.csdnimg.cn/20201118202544339.png)
![78.子集](https://code-thinking-1253855093.file.myqcloud.com/pics/20201118202544339.png)
认清这个本质之后,今天的题目就是一道模板题了。
@@ -225,23 +227,23 @@ if (startIndex >= nums.size()) { // 终止条件可以不加
树形结构如下:
![90.子集II](https://img-blog.csdnimg.cn/2020111217110449.png)
![90.子集II](https://code-thinking-1253855093.file.myqcloud.com/pics/2020111217110449.png)
## 递增子序列
在[回溯算法:递增子序列](https://programmercarl.com/0491.递增子序列.html)中,处处都能看到子集的身影,但处处是陷阱,值得好好琢磨琢磨!
树形结构如下:
![491. 递增子序列1](https://img-blog.csdnimg.cn/20201112170832333.png)
![491. 递增子序列1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201112170832333.png)
很多同学都会把这道题目和[回溯算法:求子集问题(二)](https://programmercarl.com/0090.子集II.html)混在一起。
**[回溯算法:求子集问题(二)](https://programmercarl.com/0090.子集II.html)也可以使用set针对同一父节点本层去重但子集问题一定要排序为什么呢**
**[回溯算法:求子集问题(二)](https://programmercarl.com/0090.子集II.html)也可以使用set针对同一父节点本层去重但子集问题一定要排序为什么呢**
我用没有排序的集合{2,1,2,2}来举个例子画一个图,如下:
![90.子集II2](https://img-blog.csdnimg.cn/2020111316440479.png)
![90.子集II2](https://code-thinking-1253855093.file.myqcloud.com/pics/2020111316440479.png)
**相信这个图胜过千言万语的解释了**
@@ -257,7 +259,7 @@ if (startIndex >= nums.size()) { // 终止条件可以不加
如图:
![46.全排列](https://img-blog.csdnimg.cn/20201112170304979.png)
![46.全排列](https://code-thinking-1253855093.file.myqcloud.com/pics/20201112170304979.png)
**大家此时可以感受出排列问题的不同:**
@@ -270,7 +272,7 @@ if (startIndex >= nums.size()) { // 终止条件可以不加
树形结构如下:
![47.全排列II1](https://img-blog.csdnimg.cn/20201112171930470.png)
![47.全排列II1](https://code-thinking-1253855093.file.myqcloud.com/pics/20201112171930470.png)
**这道题目神奇的地方就是used[i - 1] == false也可以used[i - 1] == true也可以**
@@ -278,21 +280,21 @@ if (startIndex >= nums.size()) { // 终止条件可以不加
树层上去重(used[i - 1] == false),的树形结构如下:
![47.全排列II2.png](https://img-blog.csdnimg.cn/20201112172230434.png)
![47.全排列II2.png](https://code-thinking-1253855093.file.myqcloud.com/pics/20201112172230434.png)
树枝上去重used[i - 1] == true的树型结构如下
![47.全排列II3](https://img-blog.csdnimg.cn/20201112172327967.png)
![47.全排列II3](https://code-thinking-1253855093.file.myqcloud.com/pics/20201112172327967.png)
**可以清晰的看到使用(used[i - 1] == false),即树层去重,效率更高!**
本题used数组即是记录path里都放了哪些元素同时也用来去重一举两得。
# 去重问题
# 去重问题
以上我都是统一使用used数组来去重的其实使用set也可以用来去重
在[本周小结!(回溯算法系列三)续集](https://programmercarl.com/回溯算法去重问题的另一种写法.html)中给出了子集、组合、排列问题使用set来去重的解法以及具体代码并纠正一些同学的常见错误写法。
在[本周小结!(回溯算法系列三)续集](https://programmercarl.com/回溯算法去重问题的另一种写法.html)中给出了子集、组合、排列问题使用set来去重的解法以及具体代码并纠正一些同学的常见错误写法。
同时详细分析了 使用used数组去重 和 使用set去重 两种写法的性能差异:
@@ -304,7 +306,7 @@ if (startIndex >= nums.size()) { // 终止条件可以不加
**使用set去重不仅时间复杂度高了空间复杂度也高了**,在[本周小结!(回溯算法系列三)](https://programmercarl.com/周总结/20201112回溯周末总结.html)中分析过组合子集排列问题的空间复杂度都是O(n)但如果使用set去重空间复杂度就变成了O(n^2)因为每一层递归都有一个set集合系统栈空间是n每一个空间都有set集合。
那有同学可能疑惑 用used数组也是占用O(n)的空间啊?
那有同学可能疑惑 用used数组也是占用O(n)的空间啊?
used数组可是全局变量每层与每层之间公用一个used数组所以空间复杂度是O(n + n)最终空间复杂度还是O(n)。
@@ -316,7 +318,7 @@ used数组可是全局变量每层与每层之间公用一个used数组
以输入:[["JFK", "KUL"], ["JFK", "NRT"], ["NRT", "JFK"]为例,抽象为树形结构如下:
![](https://img-blog.csdnimg.cn/2020111518065555.png)
![](https://code-thinking-1253855093.file.myqcloud.com/pics/2020111518065555.png)
本题可以算是一道hard的题目了关于本题的难点我在文中已经详细列出。
@@ -325,19 +327,19 @@ used数组可是全局变量每层与每层之间公用一个used数组
本题其实是一道深度优先搜索的题目,但是我完全使用回溯法的思路来讲解这道题题目,**算是给大家拓展一下思维方式,其实深搜和回溯也是分不开的,毕竟最终都是用递归**。
# 棋盘问题
# 棋盘问题
## N皇后问题
## N皇后问题
在[回溯算法N皇后问题](https://programmercarl.com/0051.N皇后.html)中终于迎来了传说中的N皇后。
下面我用一个3 * 3 的棋盘,将搜索过程抽象为一棵树,如图:
![51.N皇后](https://img-blog.csdnimg.cn/20201118225433127.png)
![51.N皇后](https://code-thinking-1253855093.file.myqcloud.com/pics/20201118225433127.png)
从图中,可以看出,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。
那么我们用皇后们的约束条件,来回溯搜索这棵树,**只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了**。
那么我们用皇后们的约束条件,来回溯搜索这棵树,**只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了**。
如果从来没有接触过N皇后问题的同学看着这样的题会感觉无从下手可能知道要用回溯法但也不知道该怎么去搜。
@@ -361,7 +363,7 @@ used数组可是全局变量每层与每层之间公用一个used数组
因为这个树形结构太大了,我抽取一部分,如图所示:
![37.解数独](https://img-blog.csdnimg.cn/2020111720451790.png)
![37.解数独](https://code-thinking-1253855093.file.myqcloud.com/pics/2020111720451790.png)
解数独可以说是非常难的题目了,如果还一直停留在一维递归的逻辑中,这道题目可以让大家瞬间崩溃。
@@ -380,22 +382,27 @@ used数组可是全局变量每层与每层之间公用一个used数组
以下在计算空间复杂度的时候我都把系统栈(不是数据结构里的栈)所占空间算进去。
子集问题分析:
* 时间复杂度O(2^n)因为每一个元素的状态无外乎取与不取所以时间复杂度为O(2^n)
* 空间复杂度O(n)递归深度为n所以系统栈所用空间为O(n)每一层递归所用的空间都是常数级别注意代码里的result和path都是全局变量就算是放在参数里传的也是引用并不会新申请内存空间最终空间复杂度为O(n)
排列问题分析:
* 时间复杂度O(n!)这个可以从排列的树形图中很明显发现每一层节点为n第二层每一个分支都延伸了n-1个分支再往下又是n-2个分支所以一直到叶子节点一共就是 n * n-1 * n-2 * ..... 1 = n!。
* 空间复杂度O(n),和子集问题同理。
组合问题分析:
* 时间复杂度O(2^n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
* 空间复杂度O(n),和子集问题同理。
N皇后问题分析
N皇后问题分析
* 时间复杂度O(n!) 其实如果看树形图的话直觉上是O(n^n)但皇后之间不能见面所以在搜索的过程中是有剪枝的最差也就是On!n!表示n * (n-1) * .... * 1。
* 空间复杂度O(n),和子集问题同理。
解数独问题分析:
* 时间复杂度O(9^m) , m是'.'的数目。
* 空间复杂度O(n^2)递归的深度是n^2
@@ -403,7 +410,7 @@ N皇后问题分析
**一般说道回溯算法的复杂度,都说是指数级别的时间复杂度,这也算是一个概括吧!**
# 总结
# 总结
**[「代码随想录」](https://img-blog.csdnimg.cn/20200815195519696.png)历时21天14道经典题目分析20张树形图21篇回溯法精讲文章从组合到切割从子集到排列从棋盘问题到最后的复杂度分析**,至此收尾了。
@@ -412,11 +419,12 @@ N皇后问题分析
可以说方方面面都详细介绍到了。
例如:
* 如何理解回溯法的搜索过程?
* 什么时候用startIndex什么时候不用
* 如何去重?如何理解“树枝去重”与“树层去重”?
* 什么时候用startIndex什么时候不用
* 如何去重?如何理解“树枝去重”与“树层去重”?
* 去重的几种方法?
* 如何理解二维递归?
* 如何理解二维递归?
**这里的每一个问题,网上几乎找不到能讲清楚的文章,这也是直击回溯算法本质的问题**
@@ -424,11 +432,11 @@ N皇后问题分析
此时回溯算法系列就要正式告一段落了。
**录友们可以回顾一下这21天每天的打卡每天在交流群里和大家探讨代码最终换来的都是不知不觉的成长**
**录友们可以回顾一下这21天每天的打卡每天在交流群里和大家探讨代码最终换来的都是不知不觉的成长**
同样也感谢录友们的坚持,这也是我持续写作的动力,**正是因为大家的积极参与,我才知道这件事件是非常有意义的**。
回溯专题汇聚为一张图:
回溯专题汇聚为一张图:
![](https://code-thinking-1253855093.file.myqcloud.com/pics/20211030124742.png)