首先考虑一道奥数题目:

□□□ + □□□ = □□□,要将数字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. 20145311王亦徐《网络对抗技术》MAL_逆向与Bof基础

    20145311王亦徐<网络对抗技术>MAL_逆向与Bof基础 实践目标 运行一个可执行文件,通过逆向或者Bof技术执行原本不应该执行的代码片段采用的两种方法: 1.利用foo函数的Bof ...

  2. object Add(object Before, object After, object Count, object Type);

    [表达式] .Add(Before, After, Count, Type) [表达式] 一个代表 Sheets 对象的变量. Before指定工作表的对象,新建的工作表将置于此工作表之前. Afte ...

  3. Sql Server 创建表添加说明

    http://bbs.csdn.net/topics/340184487 在此感谢 提供参考 CREATE TABLE ToPayFee (    Id INT IDENTITY(1,1) PRIMA ...

  4. python如何安装第三方库

    1.python集成开发环境pycharm如何安装第三方库 http://blog.csdn.net/qiannianguji01/article/details/50397046 有的时候安装不上第 ...

  5. Network Simulator for P4(NSP4) src内容介绍

    Structure What's NSP4? src source code introduction What's NSP4? NSP4是一个用于P4的网络仿真工具,旨在简化P4的环境部署和运行,将 ...

  6. NOI 7614 最低通行费(多段图最短路)

    http://noi.openjudge.cn/ch0206/7614/ 题意: 有一个N*N的正方形网格,商人从网格的左上角进,右下角出.每穿越中间1个小方格,都要花费1个单位时间.商人必须在(2N ...

  7. UVa 3349 Snowflake Snow Snowflakes(Hash)

    http://poj.org/problem?id=3349 题意: 给出n片雪花留个角的长度,要求判断是否有一样的雪花. 思路: Hash表的应用. 首先将每个雪花所有角的总长计算出来,如果两片雪花 ...

  8. Jquery 移除某一个div下面的所有img图片

    function one(){ $('#dimg img').remove(); }

  9. MUI --- h.js无效

    <!doctype html> <html> <head> <meta charset="UTF-8"> <title> ...

  10. 升级php7一些需要注意的地方

    1.升级过程涉及代码的主要处理的就是几个扩展(mysql.mssql .mcrypt.ereg)使用到的一些废弃函数(call_user_method.call_user_method_array等) ...