首先考虑一道奥数题目:

□□□ + □□□ = □□□,要将数字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. SQL学习之SQL注入总结

    Sql注入定义: 就是通过把sql命令插入到web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行的sql命令的目的. sql注入分类: 基于联合查询 基于错误回显 基于盲注,分时间盲 ...

  2. 01: MySql简介

    MySQL其他篇 目录: 参考网站 1.1 数据库介绍 1.2 视图 1.3 触发器 1.4 事物 1.1 数据库介绍返回顶部 1.什么是数据库? 1. 数据库(Database)是按照数据结构来组织 ...

  3. 20145216史婧瑶《网络对抗》Web基础

    20145216史婧瑶<网络对抗>Web基础 实验问题回答 (1)什么是表单 表单在网页中主要负责数据采集功能.一个表单有三个基本组成部分: 表单标签.表单域.表单按钮. (2)浏览器可以 ...

  4. 关于cp命令的编写

    关于cp命令的编写 娄老师在课上详细的讲了命令who的编写过程~对此,我很有启发!于是想亲自动手试试~ 有什么不足的地方请大家提出来! Learning by doing ~ 做中学,真的只有自己动手 ...

  5. 小工具:word表格文字转化成insert语句

    群里的一个朋友有个需要,要让把word里的表格数据插入到数据库里面. 我的思路是,把格式化的数据转成insert语句,然后去执行就可以了. 要求的insert语句格式是:'insert into xx ...

  6. Email移动的原理

    1.从数据库中得到被移动邮件的uid: 2.选择移动邮件所属folder,即SelectFolder; 3.调用copymessage(path,vmime::net::messageset::byU ...

  7. jquery hover最后解决 - 不再疑惑 - 例子在这里

    hover具有动画累计的bug, 可以使用 stop 或 filter(:not(:animated))来消除, 但是, 即使这样, 当鼠标反复滑入或滑出的时候, 虽然没有动画累计的问题, 但是 下面 ...

  8. luogu3261 懒惰左偏树 [JLOI2015]城池攻占

    目录 题目 思路 错误&&反思 代码 题目 luogu 原来左偏树真的能懒惰下放 那这篇博客应该要咕咕了 一开始我按照那篇博客想了一下,感觉emm,还是瞄了一眼看到了pushdown ...

  9. 51nod 1003 阶乘后面0的数量

    每一个 2 与一个 5 相乘,结果就增加一个零. 所以求 n! 后面的连续零的个数,其实就是求其中相乘的数含有因子每对因子 2 与 5  的个数. 又因为从1到某个数,所含 2 的个数比 5 多,所以 ...

  10. 解决 mininet gave up after 3 retries 问题

    解决 mininet gave up after 3 retries 问题 在通过mn启动mininet的时候遇到了如下问题: *** Creating network *** Adding cont ...