用DFS 解决全排列问题的思想详解

首先考虑一道奥数题目:

□□□ + □□□ = □□□,要将数字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

原文地址:https://www.cnblogs.com/curo0119/p/8414195.html

时间: 2024-10-06 13:39:28

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

解决Eclipse SVN文件冲突详解

在使用Eclipse SVN插件进行团队开发的过程,假设开发人员A和B都获取了同一个文件的最新版本(假如版本号为8),并都对其进行了改动,成员A已经提交了自己所作的改动(版本号变为9),如果此时成员B想要提交自己的改动,就极有可能与成员B已经提交的改动产生冲突. 如下图所示,在Eclipse SVN同步视图中的Test.java就是一个产生了版本冲突的文件,那么我们该如何解决SVN的文件冲突呢? 1.解决简单的文件版本冲突 对于产生版本冲突的文件,如果两个人改动的不是同一处位置,例如成员A只改动

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

此题题意很好懂:  给你N个数,Q个操作,操作有两种,‘Q a b ’是询问a~b这段数的和,‘C a b c’是把a~b这段数都加上c. 需要用到线段树的,update:成段增减,query:区间求和 介绍Lazy思想:lazy-tag思想,记录每一个线段树节点的变化值,当这部分线段的一致性被破坏我们就将这个变化值传递给子区间,大大增加了线段树的效率. 在此通俗的解释我理解的Lazy意思,比如现在需要对[a,b]区间值进行加c操作,那么就从根节点[1,n]开始调用update函数进行操作,如果

动态规划思想详解及示例实现

本文以两个具体例子详细剖析动态规划算法设计思想,主要参考圣经<算法导论>,加上自己的一些理解,主要是附上了一些具体实现过程,所以希望能对大家有所帮助. #_*_ coding:utf-8 _*_ import numpy as np def MemoizedCutRodAux(p,n,r,s): if r[n]>=0: return r[n] if n==0: q=0 c=0 else: q=-1 c=-1 for i in range(1,n+1): if q<(p[i]+Mem

二叉树的线索化算法思想详解

二叉树的线索化,这几天以来我很难掌握,今天终于想通了,哈哈,首先我们来看看二叉树线索化之后会变成什么样子,这里我们以图中的二叉树为例,图如下: 画的太糙,各位看官讲究着看吧- -.所谓二叉树的线索化,就是当一个节点的左右指针为空时,就让它的左右指针指向该节点的前驱或者后继(一般来说左指针指向前驱,右指针指向后继).这里不论指向前驱或者后继,我们都应该线索化时,至少要明确两个节点指针的值,当前节点和当前节点的前驱/后继.这也是线索化的两种思路: 保存前驱:访问当前节点时若当前节点的左指针为空,则令

unity3d 获取游戏对象详解

原文地址:http://www.xuanyusong.com/archives/2768 我觉得Unity里面的Transform 和 GameObject就像两个双胞胎兄弟一样,这俩哥们很要好,我能直接找到你,你也能直接找到我.我看很多人喜欢在类里面去保存GameObject对象.解决GameObject.Find()无法获取天生activie = false的问题.     private GameObject root ; 我觉得你最好不要保存GameObject ,而是去保存Transf

C# 网络编程之豆瓣OAuth2.0认证详解和遇到的各种问题及解决

        最近在帮人弄一个豆瓣API应用,在豆瓣的OAuth2.0认证过程中遇到了各种问题,同时自己需要一个个的尝试与解决,最终完成了豆瓣API的访问.作者这里就不再吐槽豆瓣的认证文档了,毕竟人家也不容易.但是作者发现关于豆瓣OAuth认证过程的文章非常之少,所以想详细写这样一篇文章方便后面要做同样东西的人阅读.希望文章对大家有所帮助,尤其是想做豆瓣API开发的初学者. (文章中蓝色字表示官方文档引用,红色字是可能遇到问题及注意,黑色字是作者叙述) 一.误区OAuth1.0认证过程    

shared pool 和buffer pool 详解(之二, Cache Buffers LRU Chain、Cache Buffers LRU Chain闩锁竞争与解决)

[深入解析--eygle]学习笔记 1.1.2  Cache BuffersLRU Chain闩锁竞争与解决 当用户进程需要读数据到Buffer Cache时或Cache Buffer根据LRU算法进行管理等,就不可避免的要扫描LRU  List获取可用Buffer或更改Buffer状态,我们知道,Oracle的Buffer Cache是共享内存,可以为众多并发进程并发访问,所以在搜索的过程中必须获取Latch(Latch是Oracle的一种串行锁机制,用于保护共享内存结构),锁定内存结构,防止

Redis实战和核心原理详解(5)使用Spring Session和Redis解决分布式Session跨域共享问题

Redis实战和核心原理详解(6)使用Spring Session和Redis解决分布式Session跨域共享问题 前言 对于分布式使用Nginx+Tomcat实现负载均衡,最常用的均衡算法有IP_Hash.轮训.根据权重.随机等.不管对于哪一种负载均衡算法,由于Nginx对不同的请求分发到某一个Tomcat,Tomcat在运行的时候分别是不同的容器里,因此会出现session不同步或者丢失的问题. 实际上实现Session共享的方案很多,其中一种常用的就是使用Tomcat.Jetty等服务器提

回溯算法详解[力扣46:全排列]

解决一个回溯问题,实际上就是一个决策树的遍历过程.你只需要思考 3 个问题: 1.路径:也就是已经做出的选择. 2.选择列表:也就是你当前可以做的选择. 3.结束条件:也就是到达决策树底层,无法再做选择的条件. 如果你不理解这三个词语的解释,没关系,我们后面会用「全排列」和「N 皇后问题」这两个经典的回溯算法问题来帮你理解这些词语是什么意思,现在你先留着印象. 代码方面,回溯算法的框架: result = [] def backtrack(路径, 选择列表): if 满足结束条件: result