C++不知算法系列之解析回溯算法中的人文哲学

news2024/11/25 6:34:03

1. 前言

回溯算法让我想起“退一步海阔天空”的名言。当事情的发展到了绝境或是边缘时,可以试着后退一步,换一个方向、换一种策略,或许会看到新的出路或生机。

回溯算法的精髓:无所畏惧而不固执,善于在变通中迂回。故回溯算法也可称为试探算法。

万事万物之间必然相通而有共性,曲径相通,优秀算法思想中必也有人文哲学的韵意,想来,回溯思想是有”车到山前必有路“的励志!

本文通过几个案例,和大家一起聊聊,颇具有人生哲学韵味的回溯算法。

2. 深入回溯

2.1 递归回溯框架

回溯算法的流程如同下棋:

“举棋不定”这句成语源于下棋,缘何如此?因为每一步都有多个选择,不到最后无法知道哪一个选择是最好的,因而不定。

回溯算法则告诉我们,勇敢走出每一步,确实走不下去了,后退一步,换一个选择。回溯的关键点是可以后退,但下棋是生活,定是“落棋无悔”的。

Tips:有“落棋无悔”,也有复盘一说,下完后,可以讨论某一步如果怎样,结果又会怎样。这便是回溯。

回溯是思想,递归是手段,归功于递归的特性,它能让回溯思想优雅地得以自由驰骋。当然,能让这种思想落地的手段远不止递归。

本文在实现案例中的回溯过程都借助递归实现。如果一类问题均能通过回溯算法解决,则问题的本质一定相同,或可以透过问题的表征看到他们其实都是一个模样。

递归回溯算法的框架一:

//搜索函数,参数往往是搜索的起点
int search(int k) 
{   
    //如同下棋,每一步都有多个选择,回溯说,没关系,从第一个选择开始吧! 
	for(int i=1;i<=选择数;i++){
		if(满足条件){
            //如果选择可以,存储当前的选择,如同下棋,落子!
		   保存结果
		   if(搜索到目标){
              //如果胜利了,输出 
		   	 输出解 
		   } 
		   else
              //继续下一步 
		     search(k+1);
         //回溯的关键点,如同悔棋,起子,进入下次循环(重新选择)   
		回溯到上一步并且恢复状态	  
		} 
	}	
}

递归回溯算法框架二:

int search(int k){
	if (搜索到目的)
        输出解 
	else
        for (int i=l;i<=算符种数;i++)
        	if (满足条件)
               { 
        		search(k+ 1);
        		回溯一步,恢复保存结果之前的状态
               }
}

2.2 案例剖析

2.2.1 素数环

问题描述:把从12020个数摆成一个环,要求相邻两个数的和是一个素数。如下图所示,要求第 1 个格间和最后 1 个格间是逻辑相连的。

1.png

问题分析:任何问题的解决方案,绝对不止一种,但此问题绝对算得上是典型的回溯问题。

  • 先在环的第 1个格间摆放数字 1。再从 2~20个数字中以试探的方法逐个选择,直到找到和左边数字相加为素数的数字,然后填充在第 2 个格间。

2.png

  • 重复上述过程,继续在剩下的数字中选择一个符合条件的数字依次填充在环中空着的格间。如下为填到环中第 5 个格间时的演示图。

3.png

  • 如下是填到环中第 12 个格间时的演示图。

5.png

  • 如下是填充完环中第 19个格间后的演示图,环中还有最后一个格间,数字也只剩下 20。因为 20+19=39不是一个素数,20这个数字无法填充至第 20格间。至此,是回溯算法展示其魅力的时候。

6.png

  • 回退到第 19个格间的填充,恢复 19数字的自由,重新选择 20这个数字。

7.png

  • 20 个格间只能选择19数字,还是不满足要求。需再次后退至第 19个格间的填充,因为第 19个格间用完了所有选择,只能再后退到第 18个格间的填充且恢复填在第 18格间的数字18的自由。

9.png

  • 如下图为第一种正确的填充方案。并不是一次性填充成功,而是经过多次回溯、再选择方能找到正确答案。得到答案后,第 20 个格间又可以释放填入的数字,重新选择数字或回溯到上一个格间,寻找其它的可行答案。

11.png

编程实现:

#include <iostream>
#include <cmath>
using namespace std;
//保存1~20范围内的所有数字
int nums[21]= {0};
//标记数字是否已经使用
bool isUse[21]= {0};
//存储结果
int res[21]= {0};
//所有的方案
int total=0;
/*
*初始化
*/
void init() {
	nums[0]=0;
	for(int i=1; i<=20; i++)
		nums[i]=i;
}
/*
* 判断给定的数字是不是素数
*/
bool isSs(int num ) {
	for(int i=2; i<=int( sqrt( num ) ) ; i++ ) {
		if(num % i==0)return false;
	}
	return true;
}
//打印填充方案
void show() {
	cout<<"------------------------"<<endl;
	total++;
	for(int i=1; i<=20; i++)
		cout<<res[i]<<"\t";
	cout<<endl;
}
/*
*
* 递归回溯搜索
* 参数 pos 表示环中的某一个位置
*/
void search(int pos ) {
	int sum=0;
	//每一个位置都有 20 个数字可以填充
	for(int i=1; i<=20; i++) {
		sum= nums[i]+ res[pos-1];
		if(!isUse[i] && isSs(sum) ) {
			//满足条件(可用且和左边数字相加为素数),保存结果
			res[pos]=i;
			//标志此数字已经使用
			isUse[i]=true;
			//是否搜索完毕
			if(pos==20) {
				//第 20 个格间的数字还需要保证和第 1 个格间中的数字相加的和为素数
				sum=res[20]+res[1];
				if( isSs(sum )) {
					show();
					//因方案较多,只选择前 10 个方案
					if(total==10)return;
				}
			} else {
				search(pos+1);
			}
			//回溯的关键,后退后,需要恢复已经填充数字的自由
			isUse[i]=0;
		}
	}
}
//测试
int main(int argc, char** argv) {
	init();
	search(1);
    return 0;
}

输出结果:

4.png

2.2.2 排列

问题描述:现有 n 个整数,从中任意取出 m 个数进行排列(m<n),请列出所有排列。

本题是一个排列问题,如果使用穷举法,不仅代码臃肿不堪,且因 nm 不确定,很难做到动态适应。如果运用回溯算法,则能让代码具有足够的动态性。

前文的”素数环“本质也是一个排列的问题,可以认为是对1~20个数字进行全排列,然后筛选出相邻数字为素数的排列。

则删除素数环问题中相邻数字和为素数的验证的代码,便是排列案例的代码。

#include <iostream>
#include <cmath>
using namespace std;
//从 10 个数字中选择 5 个数字的所有排列
int nums[11]= {0};
//标记数字是否已经使用
bool isUse[11]= {0};
//排列个数
int total=0;
//选择的数字个数
int m=5; 
//存储结果
int res[5]= {0};
/*
*初始化
*/
void init() {
	nums[0]=0;
	for(int i=1; i<=10; i++)
		nums[i]=i;
}
//打印方案
void show() {
	cout<<"------------------------"<<endl;
	total++;
	for(int i=1; i<=5; i++)
		cout<<res[i]<<"\t";
	cout<<endl;
}
/*
*
* 递归回溯搜索
* 参数为位置
*/
void search(int pos ) {
    //每一个位置都有 10 个数字可以填充
	for(int i=1; i<=10; i++) {
		if(!isUse[i]) {
			//只要数字没有选择过
			res[pos]=nums[i];
			//标志此数字已经使用
			isUse[i]=true;
			//是否搜索完毕
			if(pos==m) {
				show();
			} else {
				search(pos+1);
			}
             //回溯时,恢复状态
			isUse[i]=0;
		}
	}
}
int main(int argc, char** argv) {
	init();
	search(1);
	cout<<"10 个数字中选择 5 个数字的排列个数:"<<total;
	return 0;
}

输出结果:

12.png

10个数字中任意选择5个数字的排列个数可以套用排列公式计算:10!/5!=30240。和递归回溯求出来的结果一样。

2.2.3 拆分数字

问题描述:任何一个大于 1 的自然数 n ,可以拆分成若干个小于 n 的自然数之和,输入一个数字 ,输出所有组合。

算法解析流程:如下演示如果输入数字 8,求解答案的过程。会发现,和“素数环”问题有同工异曲之处。

  • 输入数字 8,则可供选择的数字为1~77 个数字,这里需知道所有求解中有一个最基本的答案,即 8可以是81相加,则环的大小最大为 8个格间。

13.png

  • 在第 1 个格间填充数字 1。则原始输入数字的值需缩减成为8-1=7。问题变成求 7的所有组合。

14.png

  • 在第 2个格间填入数字1,这里和素数环问题中的不一样,数字可以重复使用,但要求必须小于等于缩减后的数字。当然,完全可以根据需要,让数字不可重复。

15.png

  • 重复前面的过程,直到原始数字的值变为 0。便可得到第 1 个求解。

16.png

  • 得到第 1 个求解后,可以继续为第 8 个格间选择除 1 之外的其它数字,很遗憾,不再有任何数字能满足这个要求。回溯到第 7 个格间,这是很重要的一点,与"素数环"的回溯不同,这里是恢复缩减值,没有恢复原来使用过的值。

    为什么?后面通过代码解释。

18.png

  • 为第 7 个格间重新选择除 1 之外的其它数字,2 是符合要求。和素数环问题不同之处,不需要把所个格间填满,只要满足原始值缩减后为 0即可。

19.png

编程实现:

#include <iostream>
#include <cmath>
using namespace std;
//存储结果 ,初始值为 1
int res[100]= {1};
//存储输入
int num;
//总个数
int total=0;
/*
*查找后,输入结果
*/
int print(int pos) {
	cout<<num<<"=";
	for(int i=1; i<pos; i++) {
		cout<<res[i]<<"+";
	}
	cout<<res[pos]<<endl;
	//计数
	total++;
}

/*
*递归回溯算法
*target:目标数字
*pos:位置
*/
void search(int target,int pos ) {
	//每一个位置都有 1~target 个选择
	for(int i=1; i<=target; i++  ) {
		if(i==num)continue;
		res[pos]=i;
		target-=i;
		if(target==0)print(pos);
		else search(target,pos+1);
		target+=i;
	}
}
int main(int argc, char** argv) {
	cin>>num;
	search(num,1);
	cout<<num<<"可以拆分成 "<<total<<" 个表达式"<<endl;
	return 0;
}

输出结果:

20.png

结果里面会有很多重复的表达式。

为什么会这样?

原因很简单,如果相邻 2 个格间中的数字 1,2的顺序是满足条件的, 回溯后,前面格间会重填 2,因为后面的格间又是从 1 开始试探性选择,显然1是满足的。会现出2,1同样满足条件。

如何删除这些重复的方案?

只需要让后续格间的数字用前格间的数字作为起点!如1,2满足条件,回溯后,前面会重填2,因后面格间的值要以前面格间中的值为起点,所以 1 不会被选择。

//省略,在搜索函数中修改 i 的起点
void search(int target,int pos ) {
	//每一个位置都有 1~target 个选择
	for(int i=res[pos-1]; i<=target; i++  ) {
		if(i==num)continue;
		res[pos]=i;
		target-=i;
		if(target==0)print(pos);
		else search(target,pos+1);
		target+=i;
	}
}
//省略……

输出结果:

21.png

2.2. 4 八皇后问题

问题描述:在一个88 列的棋盘上,有 8 个皇后,请问让这 8 个皇后不在同一行、不在同一列、不在所有对角线上的摆放方式有多少种?

问题分析:

素数环可认为是把合适的值填充到一维数组中,八皇后问题可认为是把数字填充到二给数组中。

22.png

先可以从二维数组的 (1,1)格间开始填充,有 8 种选择,且必须保存同一行、同一列、所有对角线上没有其它皇后。

  • 同一行,即行号相同位置。

  • 同一列,即列号相同位置。

  • / 对角线,即行号加列号的值相同。\对角线,即行号减列号的绝对值相等。

除此之外,和前面案例的求解过程相似。

#include <iostream>
#include <iomanip>
#include <cmath>
using namespace std;
//二维数组,用来存储皇后位置
int nums[9][9]= {0};
//记数
int total=0;
int show() {
	total++;
	for(int i=1; i<9; i++) {
		for(int j=1; j<9; j++) {
			if( nums[i][j]!=0 ) {
				cout<<"("<<i<<","<<j<<")"<<nums[i][j] <<"\t";
			}
		}
	}
	cout<<"\n-----------------"<<endl;
}

/*
*判断位置是否符合要求
*/
bool isExist(int row,int col) {
	for(int i=1; i<9; i++) {
		for(int j=1; j<9; j++) {
			//同一行
			if(i==row && nums[i][j]!=0)return false;
			//同一列
			if(j==col && nums[i][j]!=0)return false;
			//对角线一
			if( (row+col)==(i+j) &&  nums[i][j]!=0 )return false;
			// 对角钱二
			if ( row>=col  && (row-col)==(i-j)   &&    nums[i][j]!=0 )return false;
			if ( row<col  && (col-row)==(j-i)   &&   nums[i][j]!=0  )return false;
		}
	}
	return true;
}
/*
*按行扫描遍历二维数组
*
*/
void search(int row) {
	for(int col=1; col<=8; col++) {
		if( isExist(row,col) ) {
			//如果位置可用
			nums[row][col]=col;
			if(row==8) {
				show();
			} else
				search(row+1);

			nums[row][col]=0;		
		}
	}
}
int main(int argc, char** argv) {
	search(1);
	cout<<"\共有"<<total<<"种摆放方案!";
	return 0;
}

输出结果:

23.png

如上代码,可以使用一维数组替换二维数组。

3. 总结

古人云:人生如棋,落子无悔。但人生终也不能墨守成规,如遇到困难,不妨退一步试试,也许会柳暗花明。回溯算法告诉我们,恰到好处的后退也能到达最后目的。

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

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

相关文章

基于PyTorch的图像数据归一化min-max normalization和zero-mean normalization操作实践对比分析

本文紧接前文&#xff1a; 《python基于不同方法实现特征工程常用的归一化技术Normalization对比分析》 前文主要是讲解对于数值型特征数据在特征工程或者是数据处理阶段往往需要用到数据尺度归一化操作&#xff0c;基于原生的对象和numpy第三方库分别实现了按列归一化计算和…

一文告诉你什么是开源表单系统

熟悉市场的人都知道&#xff0c;传统的表单存在效率低的瓶颈&#xff0c;无法满足当前很多企业的业务需求。开源表单系统也是顺应时代发展需求应运而生的产物&#xff0c;在提升企业办公效率和协作效率&#xff0c;推动企业数字化转型方面意义重大。今天这篇文章就告诉大家什么…

《计算机网络》——第一章知识点

考试题型: 选择题填空题判断题简答题计算题ISP:因特网服务提供者/因特网服务提供商&#xff0c;是一个向广大用户综合提供互联网接入业务、信息业务、和增值业务的公司&#xff0c;如中国电信、中国联动、中国移动等。分为主干ISP、地区ISP和本地ISP。 计算机网络∶利用通信线…

前端的实例化是什么?

我们在用vue框架的时候&#xff0c;总是会看到各种各样的实例化或者说实例化对象(实例) 所以这篇文章就谈一下什么是实例化和实例化对象(实例)&#xff0c;以及为什么要实例化的问题 前端的实例化是什么&#xff1f;vue的createApppinia的实例化为什么要实例化再一次回答上面这…

关于缓存问题的思考与总结

提到缓存&#xff0c;最容易想到的便是Redis了。Redis凭借其出色的性能表现&#xff0c;十分适合做缓存。那么为什么需要缓存这个东西以及缓存用在哪些地方呢&#xff1f; 一、基本原理 存储层次模型 注&#xff1a;也是背景来源 想要设计好的架构或者应用、程序&#xff0c…

Maven安装配置的保姆级教程

前言 下面是关于maven的一些介绍&#xff1a; maven是一个项目构建和管理的工具&#xff0c;提供了帮助管理 构建、文档、报告、依赖、scms、发布、分发的方法。可以方便的编译代码、进行依赖管理、管理二进制库等等maven的好处在于可以将项目过程规范化、自动化、高效化以及…

C++程序设计——继承与派生

更多内容可以查看系列文章C语言入门全教程&#xff08;持续更新&#xff09; 目录 前言 一、继承的概念 1.楔子 2.派生类的定义 3.继承和派生的意义 4.案例1&#xff1a;派生类的定义 二、继承方式 1.公有继承&#xff08;public&#xff09; 2.保护继承&#xff08;…

网络拓扑结构可视化呈现方案

随着数字化进程的加速&#xff0c;企业网络中设备的数量日益快速增长&#xff0c;网络规模逐渐庞大&#xff0c;组网结构、IT 环境变的无比复杂&#xff0c;需要花费大量的时间和资源去监测网络运行状态&#xff0c;诊断解决故障问题。面对不断趋向复杂化和多样化的网络规模和结…

Mac M1使用Docker报错 Failed to get D-Bus connection: No such file or directory的解决方案

0x00 前言 最近在Mac上安装docker的CentOS7镜像&#xff0c;打算开个sshd服务&#xff0c;使用命令&#xff1a; $ systemctl start sshd结果在启动sshd服务的时候提示报错&#xff1a; Failed to get D-Bus connection: No such file or directory0x01 运行环境 版本MacOS…

《Qt开发》基于QwtPolar的极坐标图绘制

QwtPolar绘制极坐标图 该示例包含以下功能: 使用QwtPolarPlot绘制极坐标曲线实现曲线的缩放和平移调整极坐标为顺时针顺序1. 创建项目 创建项目名称为QwtPolarDemo1&#xff0c;并添加一个Qt5Class类&#xff0c;命名为myPlot。 2. 配置项目 在项目——属性——C/C——常规…

【C++】——初识C++(一)

文章目录1. 进入C1.1 main&#xff08;&#xff09;函数1.2 C注释1.3 C预处理器和iostream文件1.4 头文件名1.5 名称空间1.6 使用cout进行C输出1.6.1 控制符endl1.6.2 换行符1.6.3 使用cout进行拼接1.7 cin1.8 变量1.8.1 变量名1.9 常量1.10 关键字1. 进入C 第一个程序 // my…

构建数据大屏,塑造IT运维可视化核心竞争力

随着大数据、云计算等新兴技术的发展与运用&#xff0c;在金融、交通、教育、政府等行业的信息化在飞速发展。与此同时&#xff0c;各行业的IT建设与维护管理成本也在与日俱增&#xff0c;大量的运维工作下产生庞大的运维数据&#xff0c;如何进行运维数据可视化建设也逐渐成为…

qt之smtp-demo封装与测试

简介 SMTP是一种提供可靠且有效的电子邮件传输的协议&#xff0c;它建立在FTP文件传输服务上的一种邮件服务&#xff0c;主要用于系统之间的邮件信息传递&#xff0c;并提供有关来信的通知。 SMTP的工作过程是建立连接、邮件传送、连接释放。 SMTP的默认端口是25。…

游戏思考26:使用EASTL配合共享内存做自定义STL(未完待续12/27)

文章目录一、前置学习1&#xff09;萃取&#xff08;1&#xff09;迭代器所指对象的类型-value_type<1>第一个限制-返回参数需要指明迭代器的value_type<2>第二个限制坑点-不是所有迭代器都是class type&#xff0c;原生指针就不是<3>第三个限制坑点-如果针对…

2022-12版本的Rstudio它来了,它喊我升级了

1. Rstudio喊我升级 最近每一次打开Rstudio&#xff0c;总是推送给我最新版的Rstudio&#xff1a; 它之前不是这样的&#xff0c;那时候它所在的公司还叫Rstudio&#xff0c;现在改名叫Posit了&#xff0c;就开始推送了&#xff0c;也许它认为是重大的更新&#xff0c;也许他能…

【vue面试题-vuex】

vuex1.vuex是什么&#xff1f;怎么使用&#xff1f;哪种功能场景使用它&#xff1f;2.vuex有哪几种属性&#xff1f;3.Vue.js中ajax请求代码应该写在组件的methods中还是vuex的actions中&#xff1f;4.Vuex解决了什么问题&#xff1f;5.Vuex中状态储存在哪里&#xff0c;怎么改…

【LeetCode】1739. 放置盒子

1739. 放置盒子 题目描述 有一个立方体房间&#xff0c;其长度、宽度和高度都等于 n 个单位。请你在房间里放置 n 个盒子&#xff0c;每个盒子都是一个单位边长的立方体。放置规则如下&#xff1a; 你可以把盒子放在地板上的任何地方。 如果盒子 x 需要放置在盒子 y 的顶部&…

【webpack】cjs运行时分析

准备工作&#xff08;接上篇文章的示例也可以&#xff09;&#xff1a; 1. 在index.js文件中引入任一js文件 import sum from ./sum;const result sum(1,2); console.log(result);2. sum文件 const sum (a, b) > {return ab; }export default sum3. build.js文件 const…

离子交换法深度剖析

离子交换法 是一种借助于离子交换剂上的离子和污水中的离子进行交换反应而除去污水中有害离子的方法。 离子交换法的特点 离子交换过程是一种特殊的吸附过程&#xff0c;在许多方面与吸附过程类似。 与吸附法比较&#xff0c;其特点是:它主要吸附污水中的离子化物质&#xff…

怎样做一个不会被淘汰的车载诊断工程师

步入中年&#xff0c;不可避免会接触到所谓的中年危机&#xff0c;时刻在提醒自己提高自己的护城河&#xff0c;增强核心竞争力。但是这种事情也不是靠空想&#xff0c;还是要功夫下在平时。 自己是在2016年开始接触车载诊断方面&#xff0c;从事过诊断范畴的开发、测试、偏系…