【算法】回溯:与递归,dfs的同质与分别,剪枝与恢复现场的详细理解,n皇后的回溯解法及算法复杂度分析。

news2024/11/17 11:40:16

0f39cc4c87a5417c80c0394d33d21531.gif

目录

​编辑

1.什么是回溯

2.关于剪枝

3.关于恢复现场

4.题目:二叉树的所有路径(凸显恢复现场:切实感受回溯与深搜)

问题分析

①函数设置为:void Dfs(root)

②函数设置为:void Dfs(root,path)

解题思想:使⽤深度优先遍历(DFS)求解。

代码实现

5.N后问题

问题分析

4皇后的放置方式

首先我们先在第一行进行落子:共有四种放置方式

接下来我们考虑往第二行的落子:

接下来我们考虑第三行落子:

接下来我们考虑第4行落子:

代码实现:

①对同列分析

②对于对角线的位置:

主对角线:

副对角线:

代码实现:

递归展开图

时间复杂度

空间复杂度

6.总结


1.什么是回溯

如果说递归是一个大的集合,搜索是递归的一个分支,如果说搜索是一个大的集合,回溯是搜索的分支,二者之间就差一步。

一个故事引入:

在初中的时候,那时候黑网吧很多,几个小伙伴周五放学没事就要去网吧玩几把lol,但是黑网吧为了不被查封,往往很隐蔽,那么现在有个网吧老板反侦察意识很强,把网吧放在一个迷宫后面,几个同学听说这边新网吧刚开业,冲一送四个钟,泡面随便冲,周五放学就按捺不住要去了,但是第一次去也没有路线,没有办法,只能走迷宫,越是几个哥们就出发了,来到分叉路口,哥几个决定先走一边尝试一下,万一就选对路线了呢。

52d35e6d50c742959f9ecce78fab86fa.png

然后来到第一个路口向右转,发现走不同,然后就回头到上一个路口,从新选择方向。

(从哥几个撞墙走不通回到上一个起点重新选择的这个过程就叫做回溯)。哥几个回到了路口,重新选择,这时候有个伙伴珊珊来迟,看见几个伙伴站在路口,就问,走那边,所有人都说走左边,新来的说为什么不走右边,几个弟兄回答说:左边去过了走不通,果断放弃,你要去你就自己去。 (明确知道其中一个选择不是我们想要的结果的时候,我们不走这个选择。这个就叫做剪枝)于是哥几个就用这样的方法走出迷宫,来到网吧度过了一个快乐的周五。

所以:

回溯算法实际上一个类似枚举的搜索尝试过程,在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。回溯法本质是一种深度搜索法,按条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择达不到目标,就退回一步重新选择,这种走不通就退回再走的方法为回溯法,可以说回溯就是深度搜索。

啊,这么一说回溯不就是递归吗?实际上这么说也对,因为递归当中就隐藏着回溯的过程,我们来看一下深度搜索的一种例子:比如我们二叉树的后续遍历,(遍历是一种方法,搜索是目的)

二叉树的后续遍历中,我们先访问左子树,在访问右子树,最后访问节点,是不是涉及到到左右子树在回到左右子树的过程,这实际上就是一种深度遍历,

深度优先遍历 dfs:一条道走到黑,走到不可以再往下走,回去有分支往深处走

深度优先搜索:遍历目的就是为了找值也就是搜索(可以画出决策树的问题都可以使用搜索。)

d35d19b5012c40cabdc42f38dcbd1b12.png

紫色的过程就是回溯 

3471c4a4d29d4a579a97bedc86eceaee.jpeg

这是递归和深度搜索dfs

而回溯算法的基本思想:从⼀个初始状态开始,按照⼀定的规则向前搜索,当搜索到某个状态⽆法前进 时,回退到前⼀个状态,再按照其他的规则搜索。回溯算法在搜索过程中维护⼀个逻辑状态树,通过遍历 状态树来实现对所有可能解的搜索。
回溯算法的核⼼思想:“试错”,即在搜索过程中不断地做出选择,如果选择正确,则继续向前搜 索;否则,回退到上⼀个状态,重新做出选择。回溯算法通常⽤于解决具有多个解,且每个解都需要 搜索才能找到的问题。关于多米洛问题的理解:问题衍生出子问题,子问题又衍生出相同的子问题。

那么深度搜索和回溯算法的区别在哪里呢: 

回溯本质是深搜这个说法没错,先说结论:在我的理解中,回溯和递归或者深度优先搜索算法的区别是在某些题目回溯强化了:①”剪枝的动作 ②回溯里面的‘恢复现场’的概念和实现。

就是说深度优先搜索或者遍历来说是一种穷举就是列出所有的情况,但是回溯算法中可以通过剪枝动作来规避掉一些不想要的结果,一般情况下,这就减少了工程量,虽然在算法复杂度量级上体现的不明显。 但是如果所有的结果都是我们想要的,此时剪枝动作就没有很大的意义,或者说没有办法发挥作用,那么我们的回溯和一个深度穷举效率是差不多的。也可以理解为二者是一个方法

所以:我们可以说:回溯 = 深度搜索+强化剪枝+强化恢复现场

下面我们理解一下什么叫做剪枝和恢复现场: 

2.关于剪枝

在我们家乡,二三月份果树开苞的时候,就要将那些没有果和果少的树枝剪去,让好的和大的果苞能吸收到更多的养分。我们减去的树枝首先就是不符合我们的要求才剪去了,比如没有果子。

1f2b759f09c340b9b2c42aca2502387b.png

在回溯算法中,我们将明确知道其的中一个选择不是我们想要的结果的时候,我们不走或者叫做排除这个选择。由于回溯算法的问题一般都可以转换成一颗逻辑决策树,

比如求3皇后的问题:不用知道只是介绍剪枝

4b39977af1984f398afc2e2caaecd275.png

所以在使用回溯算法时,将我们不需要的结果规避掉(直接不走那个分支,因为知道那个分支没有我们需要的结果),也就叫做剪枝,生动形象。

3.关于恢复现场

在刚才的迷宫问题中,所有人回退到路口重新进行选择的过程就是一种恢复现场的动作。

很多时候,特别是当我们学完回溯过后,有一种错觉:看到了代码中的“恢复现场”动作,我们就大脑自动反应:这个题解使用了回溯算法,实际上这种想法需要纠正一下:是因为出现了回溯我们才要想到去恢复现场。这样我们在代码实现的时候才能反应过来,出现了回溯的过程,那么就要考虑恢复现场。

所以:

什么是回溯:只要出现递归就伴随着回溯,只要出现深度优先遍历就伴随着回溯,只不过,在某些简单的题目中,我们递归调用函数传入参数的时候实际上已经是一个恢复现场的动作了。

比如二叉树的后续遍历中,

48788af6d37a4715971f55a03475a547.png

左子树遍历完,进行右子树的遍历,这个传递的参数实际上就是一种简单的恢复现场

但是如果传递的参数是一个全局变量或者是传地址调用的时候,我们在进行下步操作的时候就要考虑一下需不需要恢复现场了。

有了上面的铺垫大家心里应该对回溯有了一点认识,接下来我们来一道简单的题目来配合理解一下上面的概念。

4.题目:二叉树的所有路径(凸显恢复现场:切实感受回溯与深搜)

71afda172607432599306ad360304d30.png

问题分析

首先我们需要两个数组,一个用来存储所有的路径,也就是最终的结果。一个就是我们保存我们的单条路径。

  • 1. 如果当前节点不为空,就将当前节点的值加⼊路径 path 中,否则直接返回;
  • 2. 判断当前节点是否为叶⼦节点,如果是,则将当前路径加⼊到所有路径的存储数组 paths 中;
  • 3. 否则,将当前节点值加上 "->" 作为路径的分隔符,继续递归遍历当前节点的左右⼦节点。
  • 4. 返回结果数组。

①函数设置为:void Dfs(root)

b04f6cb7d15344818817c4b48a206100.png

9d60bc3407ab47ffb7d1f1c750aae704.png

②函数设置为:void Dfs(root,path)

 8c910be5c0ae451bb19f16dd1ae41dc9.png

剪枝的体现:在判断叶子节点的时候,如果当前节点的左右节点不为空我们就进入,为空不满足我们的条件,我们就不进入。这道题目剪枝不剪枝都可以,不剪枝可能就理解成深度搜索算法,有个剪枝也可以进一步理解为回溯。

500017e189144b298767e199f60a6ee3.png

解题思想:使⽤深度优先遍历(DFS)求解。

路径以字符串形式存储,从根节点开始遍历,每次遍历时将当前节点的值加⼊到路径中,如果该节点
为叶⼦节点,将路径存储到结果中。否则,将 "->" 加⼊到路径中并递归遍历该节点的左右⼦树。

代码实现

09e3d31c3150472098a1659605cc025f.png

  void dfs(struct TreeNode* root,char* path,int len , char** str,int * strcount)
    {
        assert(root);
         sprintf(path+len,"%d",root->val);
         len = len+1;
         
        if(root->left==NULL&&root->right==NULL)
        {
           
           str[*strcount] = path;
           *strcount++;
           return;
        }
         
        if(root->left)
        {
          sprintf(path+len,"->");
          len = len+1;
          dfs(root->left,path,len,str,strcount);
        }
         if(root->right)
        {
          sprintf(path+len,"->");
          len = len+1;
          dfs(root->right,path,len,str,strcount);
        }
    {
        
    }
    }

    char** str = (char**)malloc(sizeof(char*)*n);//定义一个字符串数组来存储路径
 char *path = (char*)malloc(1001);

int len = 0;
int strcount = 0;
   dfs(root,path,len,str,&strcount);
   free(path);
   path = NULL;
   return str;

 C语言代码目前有点问题,不过可以提供参考。

5.N后问题

. - 力扣(LeetCode)

bb6fb28633e94bd985e482d46f8f6659.png

问题分析

简单点:有几个皇后就是一个几乘以几的棋盘,然后当我们在一个位置放上一个棋子后,同行同列,同对角线不可以放第二枚棋子,然后要求n个棋子有多少种方法。

算法思想:①对每个位置进行枚举,也就是一个小格子一个小格子的去判断,就是对于每个位置试着放,看能不能放,如果是从1到N个位置进行判断,时间复杂度为:O(N^3)

89e6da9b890b4a89990993a8fbf86ff4.png

第一次放第一个格子,,然后去判断N^2-1个格子可不可以放置

第二次放在第二个格子,然后去判断剩下N^2-2个格子可不可以放置

时间复杂度应该为O(N^3)

②以行为单位,去看每一行的棋子应该怎么放:每一行落子后就考虑下一行,然后当我们行数来到n行的时候,就得到一个合理的结果了

4皇后的放置方式

首先我们先在第一行进行落子:共有四种放置方式

70a7c489e9174935ad18e8129a876d71.png

接下来我们考虑往第二行的落子:

第二行依然是四种情况,但是对角线和同行同列排除不放:

1e9f65a048fc42c4a36fdf669894131f.png

接下来我们考虑第三行落子:

 5f7736aa113147768fdf31c0ca504595.png

接下来我们考虑第4行落子:

fa720ec799e145ffb671aa11abcf3651.png

按照这样的方法就可以得到结果,我们的4皇后的结果是以上两种方法。最后得到的这个树状图就是经过剪枝的效果。

代码实现:

首先创建一个N*N的棋盘,然后在每一行试着放上一个皇后,判断是否可以放;

①对同列分析

首先检查该位置所在的列有没有皇后。这里我们这样用一个bool类型的数组来保存每一列的情况,某一列上有皇后,我们就保存为true,没有就保存folse:由于放置皇后位置这一列的的列数是一样的

8e4b278f6efe44cd8252d77001d6f183.png

那么通过列数作为数组下标,访问这个数组的值,就可以知道这一行有没有元素。

704d177359bc4b73b59ee99b78d35856.png

②对于对角线的位置:

主对角线:

 处于同一条对角线的格子,都在一条直线上:y = X+b

也就是说,处于一条对角线上的格子都满足:y-x = b,

那么我们就可以像列一样将对角线的情况用一个数组存储起来,然后当放置皇后的时候,用该点的横纵坐标来计算出当前在那条对角线上,然后通过数组就可以知道这条对角线上有没有皇后。

但是由于数组的下标不能出现负数,所以这里计算的时候,可以将等式两边同时加上n,x相当于将棋盘向上平移n.

y-x+n = b+n

65b4145f05af41ac9f742a36e9765f43.png

副对角线:

34e4018b071448889207bd3c36590c1f.png

代码实现:

#include<stdio.h>
#include<stdbool.h>

#define N 4//定义宏,控制皇后数量
bool CheckCol[N];//0 1 2 3 列的情况
bool CheckDig[3*N];//主对角线
bool CheckBig[2*N];//副对角线

char pan[N][N];//定义棋盘大小

int num = 0;//全局变量,记录方案
//初始化棋盘函数,将棋盘初始化为.
void InitPan()
{
	
	int i = 0;
	int j = 0;
	for (i = 0; i < N; i++)
	{
		for (j = 0; j < N; j++)
		{
			pan[i][j] = '.';
		}
	}

}
//打印棋盘函数,用于对棋盘进行输出
void PrintPan()
{
	
	int i = 0;
	int j = 0;
	for (i = 0; i < N; i++)
	{

		for (j = 0; j < N; j++)
		{
			
			printf("%c ", pan[i][j]);
			
		}
		printf("\n");
	}

}

void dfs(int row)
{
	if (row == N)
	{
		num++;
		PrintPan();
		printf("________________\n");
		return;
	}
	for (int col = 0; col < N; col++)
	{
		if (!CheckCol[col] && !CheckDig[row - col + N] && !CheckBig[row + col])//剪枝
		{
			pan[row][col] = 'Q';
			CheckCol[col] = CheckDig[row - col + N] = CheckBig[row + col] = true;
			
			dfs(row + 1);
			//恢复现场,回退到了上一行
			pan[row][col] = '.';
			CheckCol[col] = CheckDig[row - col + N] = CheckBig[row + col] = false;
		
		}
	}
}

int main()
{
	InitPan();//初始化一下棋盘
	PrintPan();
	printf("________________\n");
	dfs(0);
	printf("共有%d种方案\n",num);
	return 0;
}

 8b7365e5ae3c40049fda95e2ad7b21ec.png

69d150097d09425f95fe585576da6816.png

递归展开图

f211591bd3e94c988a2b2178807f6ee2.png

时间复杂度

时间复杂度是一个稳健的保守预期,就是一般只关注最坏的情况,算法复杂度和算法调用中执行的基本语句的次数成正比。

最坏的情况:第一行有N中方法,第一行的每一种方法都匹配第2行的n-1中方法

第二行:至多N-1中放法

第三行:至多N-2中放法:

第N行:至多1中方法

时间复杂度为:N*N-1*n-2......*1

时间复杂度为O(N!)

空间复杂度

对算法使用的一个额外空间进行估算。

引入斐波那契数列的递归计算进行讲解:

重点:计算时间复杂度的时候时间是可以累加的,但是空间却是可以重复利用的

先上代码:

// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
 if(N < 3)
 return 1;
 
 return Fib(N-1) + Fib(N-2);
}

我们在计算其时间复杂度的时候我们这样来理解这个算法的调用的:

1d1b16d951af4cf9b0b468bad324172c.png

在这个时候我们理解的是:递归调用函数是一起调用的,但是在真正的递归在内存中跑起来却不是这样调用的:

我们以Fib(4)举例讲解:

abef2cc6e18840f589d839c77ddcac2c.png

然后后面的调用都是使用这片空间,我们如果在程序中调试去看,我们的N的值应该会这样变化:4-3-2-1-3-4-2-4:

adf6447d2aec4859ae1093fc967cb684.gif

那么当我们有n个递归:

4897ae4420d4402bbb3183fb02287db4.png

是不是只会总的开辟N个空间从N到1,那么空间复杂度就为O(N)

时间一去不复返,空间可以重复再利用。函数最多递归n次,也就是开辟n次栈帧空间,

da0aef1fbbb54567b245b0c978b87e02.png

所以时间复杂度为O(N)

6.总结

本文先带大家了解什么是回溯:走不通回头,然后给出了第一个回溯算法的定义,然后给大家区分了递归和深度搜素和回溯的区别,然后引出了对回溯的剪枝和恢复现场的讲解,接着通过二叉树路径这道简单题目让大家对以上概念得到运用和更深入理解,最后使用递归解决了n皇后的问题,分析了时间复杂度和空间复杂度。创作不易,希望大家多多指教,如果觉得今天讲解的有学到东西,可以留下一个三连,持续关注后续的文章。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1595009.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

unity记一下如何播放动画

我使用的版本是2022.3.14fc 展开你的模型树&#xff0c;是会出现这个三角形的东西的 然后在资源面板创建一个animation controller 进去之后&#xff0c;把三角形拖进去&#xff0c;就会出现一个动画&#xff0c;然后点击他 在左侧给他创建这么个状态名字&#xff0c;类型…

【JavaEE多线程】理解和管理线程生命周期

目录 ThreadThread类的常用构造方法Thread类的常见属性启动一个线程-start()终止一个线程等待一个线程-join()线程的状态 Thread Thread 就是在 Java 中&#xff0c;线程的代言人。系统中的一个线程&#xff0c;就对应到 Java 中的一个 Thread 对象。围绕线程的各种操作&#…

webrtc中的Track,MediaChannel,MediaStream

文章目录 Track,MediaChannel,MediaStream的关系MediaStream的创建流程创建VideoChannel的堆栈创建VideoStream的堆栈 sdp中媒体参数信息的映射sdp中媒体信息参数设置体系参数设置流程参数映射体系 Track,MediaChannel,MediaStream的关系 Audio/Video track&#xff0c;MediaC…

一款酷黑风个人html引导页

一款酷黑风个人html引导页&#xff0c;如果想要修改的话&#xff0c;请在index.html文件修改图片位置在&#xff0c;images文件夹背景音乐在music文件夹手机端在m文件夹 源码下载 一款酷黑风个人html引导页

Windows Server 2016虚拟机安装教程

一、VMware Workstation虚拟机软件的下载 官网下载入口&#xff1a;​​​​​​Download VMware Workstation Pro - VMware Customer Connect​​​​​ 下载好之后自己看着提示安装软件就好. 二、镜像文件的下载 下载网站入口&#xff1a;MSDN, 我告诉你 - 做一个安静…

【SERVERLESS】AWS Lambda上实操

通过Serverless的发展历程及带给我们的挑战&#xff0c;引出我们改如何改变思路&#xff0c;化繁为简&#xff0c;趋利避害&#xff0c;更好的利用其优势&#xff0c;来释放企业效能&#xff0c;为创造带来无限可能。 一 Serverless概述 无服务器计算近年来与云原生计算都是在…

Tool:VRAM的简介、查询电脑VRAM的常用方法

Tool&#xff1a;VRAM的简介、查询电脑VRAM的常用方法 目录 VRAM的简介 查询电脑VRAM的常用方法 1、对于Windows系统 T1、设置-系统-显示查询法 T2、使用 DirectX 诊断工具&#xff1a; T3、使用系统信息工具&#xff1a; 2、对于Linux系统 T1、使用nvidia-smi命令&…

LeetCode 1 in Python. Two Sum (两数之和)

两数之和算法思想很简单&#xff0c;即找到nums[i]和nums[j]target-(nums[i])返回[I, j ]即可。问题在于&#xff0c;简单的两层遍历循环时间复杂度为O()&#xff0c;而通过构建一个hash表就可将时间复杂度降至O(n)。本文给出两种方法的代码实现。 示例&#xff1a; 图1 两数之…

算法中的复杂度(先做个铺垫)

文章目录 定义与分类时间复杂度概念大O的渐进表示法举例情况注意内涵 空间复杂度最优解 定义与分类 复杂度&#xff1a;衡量算法效率的标准时间效率&#xff1a;衡量这个算法的运行速度&#xff0c;也就是我们常说的时间复杂度空间效率&#xff1a;衡量这个算法所需要的额外空…

Unsupervised Learning ~ Anomaly detection

unusual events vibration: 振动 Density estimation: Gaussian(normal) Distribution. standard deviation: 标准差 variance deviation sigma Mu Parameter estimation Anomaly detection algorithm 少量异常样本点的处理经验 algorithm evaluation skewed datatsets:…

【 信息技术教资面试备战】

信息技术教资面试 教育事业&#xff0c;是一项终身事业&#xff0c;是从胎教开始到临终教育的一个循序渐进的过程。为此&#xff0c;教育艺术应当是人类生存之光。 一、什么是信息技术教资面试 考什么&#xff1a; 信息技术教资面试主要考察的内容包括结构化面试、试讲和答辩。…

字符串常量池(StringTable)

目录 String的基本特性 String的内存分配 字符串拼接操作 intern()的使用 String的基本特性 String&#xff1a;字符串&#xff0c;使用一对""引起来表示 String声明为final的&#xff0c;不可被继承 String实现了Serializable接口&#xff1a;表示字符串是支持…

数据库:SQL分类之DQL详解

1.DQL语法 select 字段列表 from 表名列表 where 条件列表 group by 分组字段列表 having 分组后条件列表 order by 排序字段列表 limit 分页参数 基本查询 条件查询&#xff08;where&#xff09; 聚合函数&#xff08;count、max、min、avg、sum &#xff09; 分组查询&…

jenkins+docker集成harbor实现可持续集成

目录 一、前言 二、Harbor介绍 2.1 什么是Harbor 2.1.1 Harbor架构图 2.2 Harbor 特征 2.3 Harbor 核心组件 2.4 Harbor使用场景 三、Harbor部署 3.1 安装docker compose 3.1.1 安装方式一 3.2 基于python3 pip安装docker compose 3.2.1 安装python3 3.2.2 安装pyt…

Kafka 架构深入探索

目录 一、Kafka 工作流程及文件存储机制 二、数据可靠性保证 三 、数据一致性问题 3.1follower 故障 3.2leader 故障 四、ack 应答机制 五、部署FilebeatKafkaELK 5.1环境准备 5.2部署ELK 5.2.1部署 Elasticsearch 软件 5.2.1.1修改elasticsearch主配置文件 5.2…

Collection与数据结构 二叉树(三):二叉树精选OJ例题(下)

1.二叉树的分层遍历 OJ链接 上面这道题是分层式的层序遍历,每一层有哪些结点都很明确,我们先想一想普通的层序遍历怎么做 /*** 层序遍历* param root*/public void levelOrder1(Node root){Queue<Node> queue new LinkedList<>();queue.offer(root);while (!qu…

2024第十五届蓝桥杯 JAVA B组 填空题

没参加这次蓝桥杯算法赛&#xff0c;十四届蓝桥杯被狂虐&#xff0c;对算法又爱又恨&#xff0c;爱我会做的题&#xff0c;痛恨我连题都读不懂的题&#x1f62d;,十四届填空只做对一个&#xff0c;今天闲的蛋疼想看看这次比赛能做对几个。 暂时没找到题目&#xff0c;这是网上找…

【Linux】阿里云ECS搭建lnmp和lamp集群

搭建LNMP&#xff08;Linux Nginx MySQL PHP&#xff09;或LAMP&#xff08;Linux Apache MySQL PHP&#xff09;集群 创建ECS实例&#xff1a; 在阿里云控制台创建多个ECS实例&#xff0c;选择相应的操作系统和配置&#xff0c;确保这些实例在同一VPC网络内&#xff0c;…

探索ERC20代币:构建您的第一个去中心化应用

下面文章中会涉及到该资源中的代码&#xff0c;如果想要完整版代码可以私信我获取&#x1f339; 文章目录 概要整体架构流程技术名词解释ERC20智能合约web3.js 技术细节ERC20合约部署创建前端界面前端与智能合约互连运行DAPP 小结 概要 在加密货币世界中&#xff0c;ERC20代币…

<计算机网络自顶向下> P2P应用

纯P2P架构 没有或者极少一直运行的Server&#xff0c;Peer节点间歇上网&#xff0c;每次IP地址都可能变化任意端系统都可以直接通信利用peer的服务能力&#xff0c;可扩展性好例子&#xff1a;文件分发; 流媒体; VoIP类别:两个节点相互上载下载文件&#xff0c;互通有无&#…