啊哈算法之简单深度优先搜索案例

简述

本算法摘选自啊哈磊所著的《啊哈!算法》第四章第一节的内容——深度优先搜索(DFS)。其实这个名词以前听说过很多次,但是就是没有了解过这是什么东西,感觉很深奥离自己还很远,而且目前遇到的项目中一直都未曾有使用这种算法来解决问题,可能是我才疏学浅不会用吧,所以对这算法的概念和用法也知之甚少。结此学习之机会,来开始逐步学习DFS这项算法吧,文中代码使用C语言编写,博主通过阅读和理解,重新由Java代码实现了一遍,希望能够对DFS算法有比较深刻一点的认识。

从简单案例开始

输入一个数n,输出1~n的全排列,每个数只出现一次,例如输入3表示输出1~3的全排列:123、132、213、231、312、321,要求写个程序,能够满足输入一个数n(1~9)之后打印出这个1~n的全排列可能性。

解法思路

求1~n的全排列,很容易想到的是暴力枚举法,n是几就来几重循环,循环打印就是了,你比如n=3时,暴力法是这样的:

 1 public static void main(String[] args) {
 2     for(int a = 1; a <= 3; a++) {
 3         for(int b = 1; b <= 3; b++) {
 4             for(int c = 1; c <= 3; c++) {
 5                 if(a != b && a != c && b != c) {
 6                     System.out.println(a + "" + b + "" + c);
 7                 }
 8             }
 9         }
10     }
11 }

上面的代码看起来还好,但是如果要你打印1~9的数字全排列,写9层循环吗?不是不可以,只是显得不够专业而已,那有没有更好的解决办法呢?我们可以换一种思路来解决这个问题。

就拿上面输出1~3这三个数的全排列来讲,可以假设有三张扑克牌需要放入到编号为1~3的三个箱子中,每个箱子只能放一张扑克牌而且三个箱子需要放满,这样有多少种放法。

假设我们按照1~3的顺序依次放入到编号为1~3的箱子中,每次放牌都按照这个顺序进行。当尝试把123这三张扑克牌都放进编号为123的箱子中之后,再尝试放第4号的箱子,发现没有箱子,也发现没有扑克牌可用了,这就说明前面我们摆放的顺序就是一种方式,手上没扑克牌且箱子已经放满就是一种结束或者说临界条件。

接下来尝试其他可能,先4号箱子往前走,走到3号箱子前面,拿出这张牌,因为3号箱子已经放过3号扑克牌,就不能再放了,此时手中又没有别的扑克牌,于是继续往前一个箱子看,到了2号箱子,把里面的2号牌拿出,此时手中有23两张扑克牌,按照123的顺序应该先放2号牌的,但是刚刚那轮已经先放了2号牌,于是现在放3号牌。放完2号箱子,继续到下一个箱子前面,此时手中就只有2号牌,按照123的顺序自然就把2号扑克牌投入到3号箱子中,再往下走,发现到了4号的位置没有了箱子手中也没有扑克牌,于是这轮放牌又结束了,前面3个箱子中的牌顺序就是一个新的序列。

按照上面这种方式,把所有的扑克牌在处理完一轮之后又重新拿出进行下一轮顺序的放牌,如此往复,直到所有情况都遍历完。

代码实现

 1 public class DfsStart {
 2 
 3     /**
 4      * 当前要处理的序列数长度
 5      */
 6     private static int max = 3;
 7     /**
 8      * 桶,用来记录已经投出去的扑克牌牌面值
 9      */
10     private static int[] book = new int[10];
11     /**
12      * 用来存放扑克牌的箱子
13      */
14     private static int[] box = new int[10];
15 
16     /**
17      * 表示当前站在第几个箱子面前,由此执行深度遍历
18      * @param step
19      */
20     public void dfs(int step) {
21 
22         // 如果站在了最后一个箱子的下一个位置,则表示前面的箱子里都已经放好了扑克牌
23         if(step == max + 1) {
24             // 输出前面这一种可能的序列,数值从1开始
25             for(int i = 1; i <= max; i++) {
26                 System.out.print(box[i] + " ");
27             }
28             System.out.println("");
29 
30             // 打印完成后,回到上一次递归调用dfs的地方
31             return;
32         }
33 
34         // 此时分别站在第n个箱子面前,决定要放哪一张扑克牌
35         // 扑克牌数值从1开始
36         for(int i = 1; i <= max; i++) {
37 
38             // 判断扑克牌是否还在手中,这里用了桶
39             if(book[i] == 0) {
40 
41                 // 在手中的话,则将牌放到第Step个箱子中
42                 box[step] = i;
43                 // 把桶做上标记,表示这个扑克牌已经投出去了
44                 book[i] = 1;
45 
46                 // 第step个箱子已经放好扑克牌,接着往后走一步,尝试到下一个箱子里放扑克牌
47                 dfs(step+1);
48 
49                 // 如果走到最后的箱子发现已经放完一轮了,则回来的时候依次将牌收到手中,并把桶清理干净
50                 // 这一步很重要,因为不清理的话,后续就不能进行操作了
51                 book[i] = 0;
52             }
53         }
54         return;
55     }
56 
57     public static void main(String[] args) {
58         DfsStart dfsStart = new DfsStart();
59         // 从第一个箱子开始投放
60         dfsStart.dfs(1);
61     }
62 }

其中,dfs是一个递归函数,表示走到某一步的时候该做的事情,这里,每走一步,就往一个箱子投放一张手中有的扑克牌,投递完之后进行下一轮投放操作,这又是另外一个递归的开始,直到走到的下一步已经没有箱子了,表示当前已经走完了所有的箱子,手中的牌也已经投放完了,那就打印出之前那些箱子中牌的顺序,接着回去收牌,从上一次发牌的地方再尝试下一种可能。

学习总结

上面这个例子,虽然很简单,但是却包含深度优先搜索(Depth First Search, DFS)的基本模型,理解深度优先搜索的关键在于解决“当下该如何做”,至于“下一步如何做”则与“当下该如何做”是一样的,那么上面dfs这个函数就是为了解决当你在step个箱子的时候,你会怎么放扑克牌或者结束了投放操作,下一步也是这样的操作,如果要遍历所有的可能性,那可以用for循环来尝试所有的可能性。每当当前的步骤step完成后就进入下一个step(step=step-1),下一步的解决办法和当前的解决办法是一致的。

下面的代码就是深度优先搜索的基本模型:

1 void dfs(int step) {
2     Step 1. 判断边界
3     Step 2. 尝试每一种可能 for(i=1; i<max; i++) {
4         继续下一步 dfs(step - 1)
5     }
6     step 3.返回
7 }

我摘了一段WiKi上的解释:深度优先搜索算法(英语:Depth-First-Search,简称DFS)是一种用于遍历或搜索树或图的算法。沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所在边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。

通俗一点讲,DFS的思想是从一个顶点V0开始,沿着一条路一直走到底,如果发现不能到达目标解,那就返回到上一个节点,然后从另一条路开始走到底。 

DFS适合此类题目:给定初始状态跟目标状态,要求判断从初始状态到目标状态是否有解。

本文对DFS算法讲解的并不算深刻,希望后面还有类似的文章对此加以巩固。

知识扩展

利用这种算法,我们还可以解决一种问题:请将1~9依次填入方格中,使得能够将这个等式(口口口 + 口口口 = 口口口)成立,其中每个空格只能填一个数,使用过的数将不能再使用,请问有多少种这样的填写方法。

其实上面这种也可以认为是将9张扑克牌投入到9个箱子中,道理是一样的,就是边界点不一样,这里的边界点就改为等式成立就好了,顺带,我们也把这个算法给写一下,套路和上面基本是一样的。

 1 public void dfs2(int step) {
 2     // 如果站在了第10箱子的位置,则表示前面的箱子里都已经放好了扑克牌
 3     if(step == 10) {
 4         // 判断是否满足等式:口口口 + 口口口 = 口口口
 5         if(box[1] * 100 + box[2] * 10 + box[3]
 6                 + box[4] * 100 + box[5] * 10 + box[6]
 7                 == box[7] * 100 + box[8] * 10 + box[9]) {
 8             total++;
 9             System.out.print(String.format("%d%d%d + %d%d%d = %d%d%d",
10                     box[1], box[2], box[3], box[4], box[5], box[6], box[7], box[8], box[9]));
11             System.out.println("");
12         }
13 
14         // 打印完成后,回到上一次递归调用dfs2的地方
15         return;
16     }
17 
18     // 此时分别站在第n个箱子面前,决定要放哪一张扑克牌
19     // 扑克牌数值从1开始
20     for(int i = 1; i <= 9; i++) {
21 
22         // 判断扑克牌是否还在手中,这里用了桶
23         if(book[i] == 0) {
24 
25             // 在手中的话,则将牌放到第Step个箱子中
26             box[step] = i;
27             // 把桶做上标记,表示这个扑克牌已经投出去了
28             book[i] = 1;
29 
30             // 第step个箱子已经放好扑克牌,接着往后走一步,尝试到下一个箱子里放扑克牌
31             dfs2(step+1);
32 
33             // 如果走到最后的箱子发现已经放完一轮了,则回来的时候依次将牌收到手中,并把桶清理干净
34             // 这一步很重要,因为不清理的话,后续就不能进行操作了
35             book[i] = 0;
36         }
37     }
38     return;
39 }

参考资料

1、《啊哈!算法》/ 啊哈磊著. 人民邮电出版社

点赞