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

2.2 案例剖析

2.2.1 素数环

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

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

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

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

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

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

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

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

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

编程实现:

#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;
}

输出结果:

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;
}

输出结果:

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

2.2.3 拆分数字

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

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

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

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

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

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

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

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

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

编程实现:

#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;
}

输出结果:

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

为什么会这样?

原因很简单,如果相邻 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;}
}
//省略……

输出结果:

2.2. 4 八皇后问题

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

问题分析:

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

先可以从二维数组的 (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();} elsesearch(row+1);nums[row][col]=0;        }}
}
int main(int argc, char** argv) {search(1);cout<<"\共有"<<total<<"种摆放方案!";return 0;
}

输出结果:

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

3. 总结

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

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

  1. 算法系列——贝尔曼福特算法(Bellman-Ford)

    本系列旨在用简单的人话讲解算法,尽可能避免晦涩的定义,读者可以短时间内理解算法原理及应用细节.我在努力! 本篇文章编程语言为Python,供参考. 贝尔曼福特算法(Bellman-Ford) 典型最短 ...

  2. 算法系列(二):贪心算法--Huffman编码

    算法系列(二):贪心算法--Huffman编码 一.分析 问题描述: 哈夫曼编码是广泛地用于数据文件压缩的十分有效的编码方法.其压缩率通常在20%-90%之间.哈夫曼编码算法使用字符在文件中出现的频率 ...

  3. (十五)算法设计思想之“回溯算法”

    算法设计思想之"回溯算法" 回溯算法是什么? 什么问题适合用回溯算法解决? 适合回溯算法解决的问题 全排列 LeetCode:46.全排列 LeetCode:78.子集 思考题 回 ...

  4. 极客时间——数据结构与算法(39) 回溯算法:从电影《蝴蝶效应》中学习回溯算法的核心思想

    转载地址:https://time.geekbang.org/column/article/74287 我们在第 31 节提到,深度优先搜索算法利用的是回溯算法思想.这个算法思想非常简单,但是应用却非 ...

  5. JAVA算法:走迷宫回溯算法设计(JAVA版本)

    JAVA算法:走迷宫回溯算法设计(JAVA版本) 迷宫数组 int[][] maze = {                 {0, 1, 0, 0, 0},                 {0, ...

  6. C++不知算法系列之集结常规算法思想

    1. 前言 数据结构和算法是程序的 2 大基础结构,如果说数据是程序的汽油,算法则就是程序的发动机. 什么是数据结构? 指数据之间的逻辑关系以及在计算机中的存储方式,数据的存储方式会影响到获取数据的便 ...

  7. 回溯算法和递归算法_回溯算法:递归和搜索示例说明

    回溯算法和递归算法 Examples where backtracking can be used to solve puzzles or problems include: 回溯可用于解决难题或问题 ...

  8. 机器学习算法系列之K近邻算法

    本系列机器学习的文章打算从机器学习算法的一些理论知识.python实现该算法和调一些该算法的相应包来实现. 目录 K近邻算法 一.K近邻算法原理 k近邻算法 通俗解释 近邻距离的度量 k值的选择 KN ...

  9. 排序算法系列:Shell 排序算法

    概述 希尔排序(Shell Sort)是 D.L.Shell 于 1959 年提出来的一种排序算法,在这之前排序算法的时间复杂度基本都是 O(n2n^{2}n2) 的,希尔排序算法是突破这个时间复杂度 ...

最新文章

  1. 怎么关闭wordpress评论
  2. 浅谈ASP.NET的内部机制(一)
  3. python核心数据类型_Python核心数据类型-列表
  4. html jsf ajax blur,是否可以使用JSF ajax更新非JSF组件(纯HTML)?
  5. Tensorflow2下载与安装
  6. 使用扩展欧几里得算法对逆元求解
  7. php 上传微信媒体,关于微信公众号API多媒体上传
  8. 如何破解HIT的孤岛效应,一起关于患者主索引的案例分享
  9. RecyclerView使用中遇到的问题
  10. 清理计算机磁盘碎片,电脑磁盘碎片清理工具(Auslogics Disk Defrag Pro)
  11. Boss直聘怎么写出优秀的简历?
  12. Apache PHP 的gzip压缩输出的实现方法
  13. tomcat 如何查看tomcat版本及位数——tomcat笔记
  14. 乐理基础知识-1.节奏
  15. Git/Github
  16. python编程midi键盘按键错乱_键盘按键错乱怎么办?
  17. html5模拟真实摇骰子,js实现简单掷骰子效果
  18. 2022-10-16
  19. 数据库中如何将Unicode编码转换为文字?
  20. “线上+包作图+线下”培训,精通AI、PS等软件,高分论文插图轻松搞定!

热门文章

  1. go: cannot determine module path for source directory D:\go\code\SipSvrProxy\src\Common (outside GOP
  2. 计算机图形图像知识梳理,计算机图形学知识点总结
  3. 计算机毕设结束语致谢,毕业设计结束语和致谢
  4. 认识kubernetes(k8s),k8s单节点etcd部署
  5. CTF.show-mx密码2
  6. 红黑树如何快速调整到平衡态_快速多态
  7. 网络安全没有“银弹”
  8. 如何获取iOS App素材
  9. Unity小游戏-平衡大师(安卓、PC、web)2D益智类游戏 项目展示+完整项目源码
  10. RabbitMQ消息模型详解