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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容