首先考虑一道奥数题目:

□□□ + □□□ = □□□,要将数字1~9分别填入9个□中,使得等式成立。例如173+286 = 459。请输出所有合理的组合的个数。

我们或许可以枚举每一位上所有的数,然后判断每一位上的数需要互不相等且满足等式即可,但是用代码写出来需要声明9个变量且判断。
那么我们把这个问题考虑为一个求这个9个数的全排列问题,即可得到更优雅的解答方式。
首先我们考虑一个经典的全排列问题(《啊哈,算法》):

输入一个数,输出1~n的全排列。

现在我们考虑有1、2、3的3张扑克牌和编号为1、2、3的3个盒子,需要将这3张扑克牌放到3个盒子里,求其所有可能性。

  1. 首先我们考虑1号盒子,我们约定每到一个盒子面前都按数字递增的顺序摆放扑克牌。于是把1号扑克牌放到1号盒子中。
  1. 接着考虑2号盒子,现在我们手里剩下2号和3号扑克牌,于是我们可以把2号扑克牌放入2号盒子中。于是在3号盒子只剩一种可能性,我们继续把3号扑克放入3号盒子。此时产生了一种排列——{1,2,3}。
  2. 接着我们收回3号盒子中的3号扑克牌,尝试一种新的可能,此时发现别无他选。于是选择回到2号盒子收回2号扑克。
  3. 在2号盒子中我们放入3号扑克,于是自然而然的在3号盒子中只能放入2号扑克。此时产生另一种排列——{1,3,2};
  4. 重复以上步骤就能得到数字{123}的全排列。

现在我们用C语言代码描述往每个小盒子中放入所有可能扑克牌的步骤:
for(int i = 1; i <= n; i++){ a[step] = i; //将i号扑克牌放入第step个盒子中 }
a是一个装入了所有小盒子的数组,变量step表示当前正处于第step号小盒子前。i则表示扑克牌的序号。现在我们需要考虑另外一个问题,则如果一张扑克牌已经被放入别的盒子中,则不能再被放入当前盒子。因此需要一个book数组标记哪些牌已经被使用。此时我们完善上述代码。
for(int i = 1; i <= n; i++){ if(book[i] == 0){ a[step] = i; //将i号扑克牌放入第step个盒子中 book[i] = 1; // 置1表示第i号扑克牌不在手中 } }

现在对于step号盒子已经处理完,那么我们要考虑step+1号盒子。第step+1个的盒子的处理方式与第step个盒子的处理方式完全一样。因此,我们可以对上述操作做一个封装。
void dfs(int step){ //step表示当前要处理的盒子 for(int i = 1; i <= n; i++){ if(book[i] == 0){ a[step] = i; //将i号扑克牌放入第step个盒子中 book[i] = 1; // 置1表示第i号扑克牌不在手中 } } }

于是我们重新回想文章开头阐述的放置扑克牌的思路:我们在当前盒子放置完第i个扑克牌之后,便立即处理下一个盒子。于是:
void dfs(int step){ //step表示当前要处理的盒子 for(int i = 1; i <= n; i++){ if(book[i] == 0){ a[step] = i; //将i号扑克牌放入第step个盒子中 book[i] = 1; // 置1表示第i号扑克牌不在手中 dfs(step+1); //递归调用 book[i] = 0; // 非常重要,收回该盒子中的扑克牌才能进行下一次尝试。 } } }

需要注意到的是,我们需要收回每一次尝试的扑克牌i,才能进行下一次尝试。现在需要考虑最后一个问题,那就是什么时候得到一个满足要求的排列,也就是考虑终止条件。这里很容易得到,当我们处理完成第n个盒子的时候,就已经得到一个符合要求的排列了。加上终止条件的代码如下:
void dfs(int step){ //step表示当前要处理的盒子 if(step == n+1){ //输出排列 for(i = 1; i <= n; i++) printf("%d", a[i]); printf("\n"); return; } for(int i = 1; i <= n; i++){ if(book[i] == 0){ a[step] = i; //将i号扑克牌放入第step个盒子中 book[i] = 1; // 置1表示第i号扑克牌不在手中 dfs(step+1); //递归调用 book[i] = 0; // 非常重要,收回该盒子中的扑克牌才能进行下一次尝试。 } } }
现在深度优先搜索(DFS)的基本模型展现在我们眼前。其核心在于,在当前步骤要把每一种可能性都尝试一遍(使用for循环),解决完当前步骤后进入下一步。而下一步的解决方式完全等同于当前步骤的解决方法。于是可以总结出DFS的基本模型:
void dfs(int step){ *判断结束边界* 尝试每一种可能 for(i = 1; i <= n; i++){ 尝试下一步 dfs(step + 1); } return; }


好了,现在我们总结出来了DFS的基本框架,这个框架可以用于解决基于全排列所给出的一系列算法题。
下面列出一道《剑指offer》中的面试题:

输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。

输入描述:输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。

我们可以看到这道题目似乎和上面一开始说到的朴素的数字排列完全一致,但是我们要考虑到的是,输入的字符串中可能包含了字符重复。 标准解法如下:
PermutationHelp(vector<string> &ans, int k, string str) { if(k == str.size() - 1) // 结束条件 ans.push_back(str); unordered_set<char> us; //记录出现过的字符 sort(str.begin() + k, str.end()); //保证按字典序输出 for(int i = k; i < str.size(); i++){ if(us.find(str[i]) == us.end())//只和没交换过的换 { us.insert(str[i]); swap(str[i], str[k]); PermutationHelp(ans, k + 1, str); swap(str[i], str[k]); //复位 } } } vector<string> Permutation(string str) { vector<string> ans; PermutationHelp(ans, 0, str); return ans; }
可以看到,这里沿用了DFS的基本模型。k为当前步骤的指示器。为了解决字符重复问题,使用了std::unorder_set 容器存储已经交换过的元素。

例如我们输入为: {a,a,b,c,d}时,当k = 0, i = 0时, us.find(str[i]) == us.end()的结果为true,因为此时us中元素个数为0,此时将a放入无序集合中;而当k = 0, i = 1时,上述判断结果为false,此时不进行交换,i的值直接加1。


接下来我们解决一开始的奥数题似乎是易如反掌了:

□□□ + □□□ = □□□,要将数字1~9分别填入9个□中,使得等式成立。例如173+286 = 459。请输出所有合理的组合的个数。

我们只需要在dfs的基础上修改一下结束条件中的代码即可:
int total = 0; void dfs(int step){ //step表示当前要处理的盒子 if(step == 10){ //只有9个盒子 //判断是否满足等式 if(a[1] * 100 + a[2] * 10 + a[3] + a[4] * 100 + a[5] * 10 + a[6] == a[7] * 100 + a[8] * 10 + a[9]){ //满足要求,打印 total += 1; ........// 省略打印代码 } return; } for(int i = 1; i <= n; i++){ if(book[i] == 0){ a[step] = i; //将i号扑克牌放入第step个盒子中 book[i] = 1; // 置1表示第i号扑克牌不在手中 dfs(step+1); //递归调用 book[i] = 0; // 非常重要,收回该盒子中的扑克牌才能进行下一次尝试。 } } }
这里需要注意,最后输出的total需要除以2,因为 173 + 286 和 286 + 173 是同一种结果。


同样的,下面这道题目:

输入一个含有8个数字的数组,判断有没有可能把这8个数字分别放到正方体的8个顶点上,使得正方体上三组相对的面上的4个顶点的和相等。

这道题与上面的奥数题类似,相当于需要得到8个数字的所有排列。如图,假设8个顶点分别是a1,a2,a3,a4,a5,a6,a7,a8。 接着判断有没有某一个排列符合题目所给的条件,即:
a1+a2+a3+a4 = a5+a6+a7+a8 && a1 + a3 + a5 + a7 = a2 + a4 + a6 + a8 && a1 + a2 +a5 +a6 = a3 + a4 +a7 + a8
成立。

 
正方体.png

本文转载于:https://www.jianshu.com/p/897f2a9e33cd

用DFS 解决全排列问题的思想详解的更多相关文章

  1. 《手把手教你》系列技巧篇(七十一)-java+ selenium自动化测试-自定义类解决元素同步问题(详解教程)

    1.简介 前面宏哥介绍了几种关于时间等待的方法,也提到了,在实际自动化测试脚本开发过程,百分之90的报错是和元素因为时间不同步而发生报错.本文介绍如何新建一个自定义的类库来解决这个元素同步问题.这样, ...

  2. [原创] 分享我们自己搭建的微信小程序开发框架——wframe及设计思想详解

    wframe不是控件库,也不是UI库,她是一个微信小程序面向对象编程框架,代码只有几百行.她的主要功能是规范小程序项目的文件结构.规范应用程序初始化.规范页面加载及授权管理的框架,当然,wframe也 ...

  3. 【转载】KMP入门级别算法详解--终于解决了(next数组详解)

    [转载]https://blog.csdn.net/LEE18254290736/article/details/77278769 对于正常的字符串模式匹配,主串长度为m,子串为n,时间复杂度会到达O ...

  4. 线段树区间更新操作及Lazy思想(详解)

    此题题意很好懂:  给你N个数,Q个操作,操作有两种,‘Q a b ’是询问a~b这段数的和,‘C a b c’是把a~b这段数都加上c. 需要用到线段树的,update:成段增减,query:区间求 ...

  5. jquery及原生javascript对jsonp解决跨域问题实例详解

    jquery方式 前端: $.ajax({ url: 'http://m.xxx.tv/goLottery', data: { data: data }, type: 'GET', dataType: ...

  6. nginx调用php-fpm出错解决方法和nginx配置详解

    装完了nginx和php-5.5,配置好了nginx调用php后,就开始启动php-fpm. 使用下面的命令 复制代码 代码如下: /usr/local/php/sbin/php-fpm 就可以启动了 ...

  7. Bellman-ford算法与SPFA算法思想详解及判负权环(负权回路)

    我们先看一下负权环为什么这么特殊:在一个图中,只要一个多边结构不是负权环,那么重复经过此结构时就会导致代价不断增大.在多边结构中唯有负权环会导致重复经过时代价不断减小,故在一些最短路径算法中可能会凭借 ...

  8. unity3d 获取游戏对象详解

    原文地址:http://www.xuanyusong.com/archives/2768 我觉得Unity里面的Transform 和 GameObject就像两个双胞胎兄弟一样,这俩哥们很要好,我能 ...

  9. BFS和DFS详解

    BFS和DFS详解以及java实现 前言 图在算法世界中的重要地位是不言而喻的,曾经看到一篇Google的工程师写的一篇<Get that job at Google!>文章中说到面试官问 ...

随机推荐

  1. IEEE发布2017年编程语言排行榜:Python高居首位,java第三,php第八

    2017年7月18日,IEEE Spectrum 发布了第四届顶级编程语言交互排行榜.因为有各种不同语言的排行,所以 IEEE Spectrum 依据不同的变量对流行度进行了排行.据 IEEE Spe ...

  2. 斯坦福大学机器学习,EM算法求解高斯混合模型

    斯坦福大学机器学习,EM算法求解高斯混合模型.一种高斯混合模型算法的改进方法---将聚类算法与传统高斯混合模型结合起来的建模方法, 并同时提出的运用距离加权的矢量量化方法获取初始值,并采用衡量相似度的 ...

  3. IP分片丢失重传

    尽管IP分片看起来是是透明的,但有一点让人不想使用它:即使只丢失一片数据也要重传整个数据报.为什么会发生这种情况呢?     因为IP层本身没有超时重传的机制——由更高层来负责超时和重传(TCP有超时 ...

  4. oracle创建dblink的脚本

    创建dblink的脚本 create public database link wsbsbb_27(dblink名字) connect to wsbsbb(用户名) IDENTIFIED BY wsb ...

  5. 小白也能看懂的插件化DroidPlugin原理(一)-- 动态代理

    前言:插件化在Android开发中的优点不言而喻,也有很多文章介绍插件化的优势,所以在此不再赘述.前一阵子在项目中用到 DroidPlugin 插件框架 ,近期准备投入生产环境时出现了一些小问题,所以 ...

  6. Python3 ChromeDriver与Chrome版本映射表(更新至v2.43)

    Python3 ChromeDriver与Chrome版本映射表(更新至v2.43) ChromeDriver下载链接:http://chromedriver.storage.googleapis.c ...

  7. Linux 安装JDK(jdk-8u121-linux-x64.tar.gz)

    Linux 安装JDK(jdk-8u121-linux-x64.tar.gz) 一.JDK准备 1.1 文件名称 jdk-8u121-linux-x64.tar.gz 1.2 下载地址 http:// ...

  8. monit

    Monit 是一个用来监测系统状态的工具,不但可以用来监视进程.服务.文件.目录.文件系统,还可以在服务 down 掉的时候自动重启服务或者当某个进程占用过多的资源的时候自动停掉进程,并且支持 Ema ...

  9. Nodejs学习笔记(四)与MySQL交互(felixge/node-mysql)

    原文链接:http://www.cnblogs.com/zhongweiv/p/nodejs_mysql.html 介绍使用felixge/node-mysql进行SQL的增删改查以及断线重连等操作.

  10. 六角填数|2014年蓝桥杯B组题解析第七题-fishers

    六角填数 如图所示六角形中,填入1~12的数字. 使得每条直线上的数字之和都相同. 图中,已经替你填好了3个数字,请你计算星号位置所代表的数字是多少? 请通过浏览器提交答案,不要填写多余的内容. 思路 ...