这篇博文较长,且理论为主,因为我准备换种风格,以保证大家看的时候不会觉得无聊,看完如果对dfs还是一头雾水,那我也不会请你吃麻辣烫
开场:
刷了一遍leetcode,发现DFS是个特别强大,且出题率特别高的一个基础算法,第一次接触DFS是树的一个搜索算法,相对而言的是BFS。根据难度与考察频率参考表的建议,前面我对leetcode中的DFS的算法题进行了一个简单的汇总。但是并不完整,其中的树和图的一些题目,都留作以后做树的汇总时候再看,如果有兴趣的可以看这个列表。从这个列表中可以看粗,dfs在树的应用中是非常常见的。再加上之前不完全的汇总,dfs算法在leetcode中居然有26+道,比例高达近20%。
-------------------------------------------↑ 分割线↑-------------------------------------------------
在讨论什么是DFS之前,首先默认大家已经有了递归和搜索的基础。
如果基础不太好的话,我找了篇讲得比较好的关于递归的博文,并与迭代进行对比讲解,下面的延伸阅读也很不错,有助于更深的理解递归( & 尾递归)。(个人认为,递归最好自己亲手在纸上画一个栈图,过程就一目了然了)
搜索算法很多,也非常重要,level更高,dfs只是其中的一种,这里暂时不对搜索算法做出整理,只需要理解搜索的思想即可。大家联想一下安检时候的搜身,就是一种“搜索”算法,安检员mm摸遍你全身,想摸出他们想要的东东,摸法(“算法”)很多,有人喜欢乱摸,有人喜欢从上到下,从下到上,从前到后,(谁喊的好爽?魂淡(¯﹃¯)),借用某度百科的第一句话,搜索就是有目的(难道是摸你的身材好不好啊?)的穷举一个问题解空间(身体)的部分或所有的可能情况(比如毒品肯定要全摸出来),从而求出问题的解的一种方法。
----------------------------------- ↑ 跑偏了,言归真正,DFS ↑-----------------------------------------
DFS就是从上往下摸,一直摸到脚,或者摸到已经摸过的地方为止,然后换个部位继续从上往下摸,一直摸到脚,或者摸到已经摸过的地方,然后换个部位继续从上往下摸,一直。。。走遍你的全身
因此可以看粗,dfs本质还是递归,但是当递归返回时,dfs算法会回到原来的分叉点(回溯),尝试另一条没有走过的分支,继续递归,如果被告知哪一条支路不需要走(剪枝),直接pass。
从这个层面看来,dfs在树的章节被引用,实在是再形象不过了。跟教材一样,结合树 的例子,给大家讲解一下dfs的整个过程。
看图:
交代背景:
老王站在起点,有三条路通向三个村子,每个村子有三条路通向三个公司,每个公司有三条路通向三个鱼塘,老王要去承包鱼塘,但是老王并不知道他要承包的鱼塘的路径。只能一个一个找。
背景交代完毕。
毫无疑问,老王首先只能选择一个村子进行搜索,按照图中所给栗子,老王选择了第1个村子,走起。当他走到1号村子,他打电话告诉他妹子小三“我现在到1号村了”。面前又有三条路,通向三个公司,好嘛,再选一个,挨个来吧,选择1号公司,并打电话给他妹子“我现在在1号公司了”。面前有三个鱼塘,他到了1号鱼塘,跟鱼塘负责人说“这个我承包了”,并签了合同,并打电话跟他妹子描述了一下,他妹子嫌太小,负责人问:“还承包吗?”答:“有点小”。“不承包还不赶紧滚” “...”然后他交了违约金,取消了合同,又回到了1号公司(回溯)
到了2号鱼塘,blahblah,“哈?嫌这个太大啊?”,“还不赶紧滚” “...”然后他交了违约金,取消了合同,又回到了1号公司
到了3号鱼塘,blahblah,“哈?这个太远?”,“还不赶紧滚” “...”然后他交了违约金,取消了合同,又回到了1号公司
然后老王发现1号公司已经找遍了,3个鱼塘都不是他要找的,然后他继续往回走,回到了1号村子,进入了2号公司....太脏,太破,太乱...回到1号村子,进入3号公司,太臭、负责人不够帅、办公系统不是OA....
然后老王发现1号村已经找遍了,都不是他想要的,ok,选择2号村,重复以上自虐过程....直到找到2.3.1鱼塘,”我要向全世界宣布,这个鱼塘被你承包了“收工!啊?还想继续看剩下的几个啊?ok,27个鱼塘都看完了,最后承包了俩:2.3.1和3.3.3又大又干净,离家近,设备齐全,负责人还帅。完美。
“桥豆麻袋,这边原来还有4号村子啊,你看这里有条铁路。” “太远了,这个不考虑”(剪枝)
----------------------------------- ↑ DFS 理论基础完毕 DFS ↑-----------------------------------------
以上就是dfs的基本思想,有没有发现,其实老王在搜索这27个鱼塘的时候,是一条路走到黑的,每遇到岔路口,就选一条没走过的路往下走(递归),走到死胡同时,就返回到上一个岔路口(回溯),然后选择第二条没有走过的路,依此反复。。。但是有一个例外,就是这条路虽然还没走,但是你已经发现它不满足你的要求了,就可以直接把这条路给pass掉(所谓剪枝)
嗯,有人肯定该吐槽了,妈蛋,每本书都是这么讲的,还用你BB啊
我只是先给大家描述一下dfs的操作流程,八皇后问题可能某些同学对棋盘的一位数组表示法转不过来弯,因此本博文放弃采用最经典的八皇后问题,用汇总里面的一道题来结合鱼塘的栗子做一下讲解。
用[leetcode]Combination Sum吧,代码简单,又有回溯和剪枝操作。
题中意思是给你一个数组,比如[ 2,3,6,7
],再给你一个target比如7,你需要求出所有由数组中的数,组成的组合(元素可以重复),该组合满足,和为target,比如[2,2,3]、[7]都是满足条件的组合
新手拿到这种题目,可能搞不明白,这尼玛哪有路,哪有鱼塘啊?
且听且珍惜啊。
其实在你眼前有四条路,从2,3,6,7出发,去找你的“鱼塘”,你的“鱼塘"可能是[2,2,3]这里面的2,2,3就是你的“鱼塘”的路径,而每个岔路口,你都有n条路,即:所谓的“路”,即是每一位元素可供选择的个数,很多时候,往往就是题中给你的备选空间。
好了,再结合上面鱼塘的栗子,你现在在起点,你的数组里面一个元素都木有,现在要去找target 7
你要往你的数组里面先放一个元素,你有4个选择(4条路),好了,如果你是老王的话,肯定会选择2,因为他比较喜欢按顺序来,这样不乱。
你选择了第一个数:2(走到了第一个乡,还没到鱼塘),你的状态是2,不够7啊,你面前还是有4条路(2,3,6,7),老王还会选择第一个村:2,这时候你的状态是22,我擦,还有四条路,再选一个2,你的状态是222(= =///),你面前还有四条路,再选2,咦,你发现让你把2加进去之后,就超过7了,而且如果不选2的话,别的也都不行(剪枝),好吧,你发现222这条路不通(这个公司的鱼塘都不是你想要的),回到村子吧(回溯),村子是公司的上一级嘛,肯定是22,请注意红色部分后面这句“我擦,还有四条路”。对,没错,你刚才选的2,发现这条路走不通,那么这次选3,这时你的状态变成了223,眼泪哗哗的,这个鱼塘就是我想要的,后面的不用看了,都不满足条件,回到村子22,这个村子已经被你搜索遍历(其实你只搜索了22->2 和22->3,但是22->6和22->7被你直接剪枝,pass了)好了,22这条路已经走完了,再回到22的上一个状态2
选择第2条路 23,这时你面前有3条路,367,为什么没有2了呢?自己思考吧。实在想不明白可以给我留言=.=
发现23这条路是不通的,因为23之后,无论选367的哪一个都不满足条件,又回到上个状态2,选择26,其实已经不用再选了,23都不通,26 和27岂不是更不满足。好吧,2这个状态我们走完了,这是把你的状态全部抹去,选择3这条路,重复以上过程,发现3这条路不同,6这条路也不通,7是可以的。
最后得到答案。[2223]&[7]
下面我们结合代码来看
public class Solution { List<List<Integer>> result = new ArrayList<List<Integer>>(); public List<List<Integer>> combinationSum(int[] candidates, int target) { if(candidates == null || candidates.length == 0) return result; Arrays.sort(candidates);//排序是必要的,直接导致了选择3之后,就不能再回头选2了,同理选择6之后就不能选2,3了,理由自己想 List<Integer> list = new ArrayList<Integer>(); dfs(candidates,list,0,target); return result; }
//list表示你的当前状态,k表示你当前选的路 private void dfs(int[] candidates,List<Integer> list,int k ,int target){ if(target == 0){//找到你的鱼塘 List<Integer> copy = new ArrayList<Integer>(list); result.add(copy); return; } if(target < candidates[k])return;//表示如果选了当前的路,并且元素的和已经大于了target,后面就无需再看了(所谓剪枝) for(int i = k; i < candidates.length; i++){//注意这里的k,一旦上路,就不能再回头 list.add(candidates[i]);//你往你的数组里面放了一个元素i,比如现在你的状态是3 dfs(candidates,list,i,target - candidates[i]);//你面前有3~7三个选择,你继续选3 list.remove(list.size() - 1);//当你发现33(你的当前状态)这条路不通时候,你应该把你的状态改为3(所谓回溯)再选择36 } } }
这篇博文花了比较长时间。。。不知道有没有讲太清楚,中间被同学打断,状态散了。。。囧rz,不过我还是建议,如果米娜还是不明白的话,将这段代码放eclipse中调试一下,再结合上面的说辞,再不明白,我就不明白了╮(╯_╰)╭