DFS(深度优先算法)
深度优先搜索(缩写DFS)有点类似广度优先搜索,也是对一个连通图进行遍历的算法。它的思想是从一个顶点V0开始,沿着一条路一直走到底,如果发现不能到达目标解,那就返回到上一个节点,然后从另一条路开始走到底,这种尽量往深处走的概念即是深度优先的概念。
深度优先遍历使用的数据结构是栈(Stack),将访问过的节点标记后,并压入栈中,
再遍历此时跟栈顶元素相关联的节点,将其中未标记的节点标记,并压入栈中……以此类推,
当该栈顶的元素相关联的节点都被访问过了,则该元素弹出栈……直到栈空,遍历完成。
你找出一条V0到V6的道路,而无需最短路。
顺序搜索:
1.V0->V1->V4,此时到底尽头,仍然到不了V6,于是原路返回到V1去搜索其他路径;
2.返回到V1后既搜索V2,于是搜索路径是V0->V1->V2->V6,,找到目标节点,返回有解。这样搜索只是2步就到达了,但是如果用BFS的话就需要多几步
算法步骤:
1、首先将根节点放入stack中。
2、从stack中取出第一个节点,并检验它是否为目标。
如果找到目标,则结束搜寻并回传结果。
否则将它某一个尚未检验过的直接子节点加入stack中。
3、重复步骤2。
4、如果不存在未检测过的直接子节点。
将上一级节点加入stack中。
重复步骤2。
5、重复步骤4。
6、若stack为空,表示整张图都检查过了——亦即图中没有欲搜寻的目标。结束搜寻并回传“找不到目标”
广度优先搜索的缺点:在树的层次较深&子节点数较多的情况下,消耗内存十分严重。
广度优先搜索适用于节点的子节点数量不多,并且树的层次不会太深的情况。
那么深度优先就可以克服这个缺点,因为每次搜的过程,每一层只需维护一个节点。
但回过头想想,广度优先能够找到最短路径,那深度优先能否找到呢?
深度优先的方法是一条路走到黑,那显然无法知道这条路是不是最短的,所以你还得继续走别的路去判断是否是最短路?
于是深度优先搜索的缺点也出来了:难以寻找最优解,仅仅只能寻找有解。
其优点就是内存消耗小,克服了刚刚说的广度优先搜索的缺点。
LeetCode 127 单词接龙II
题目描述:
按字典 wordList 完成从单词 beginWord 到单词 endWord 转化,一个表示此过程的 转换序列 是形式上像 beginWord -> s1 -> s2 -> ... -> sk 这样的单词序列,并满足:
每对相邻的单词之间仅有单个字母不同。
转换过程中的每个单词 si(1 <= i <= k)必须是字典 wordList 中的单词。注意,beginWord 不必是字典 wordList 中的单词。
sk == endWord
给你两个单词 beginWord 和 endWord ,以及一个字典 wordList 。请你找出并返回所有从 beginWord 到 endWord 的 最短转换序列 ,如果不存在这样的转换序列,返回一个空列表。每个序列都应该以单词列表 [beginWord, s1, s2, ..., sk] 的形式返回。
示例:
输入:beginWord = "hit", endWord = "cog", wordList = ["hot", "dot", "dog", "lot","log", "cog"]
输出:[["hit","hot","dot","dog","cog"],["hit","hot","lot","log","cog"]]
解释:存在 2 种最短的转换序列:
"hit" -> "hot" -> "dot" -> "dog" -> "cog"
"hit" -> "hot" -> "lot" -> "log" -> "cog"
输入:beginWord = "hit", endWord = "cog", wordList = ["hot", "dot", "dog", "lot", "log"]
输出:[]
解释:endWord "cog" 不在字典 wordList 中,所以不存在符合要求的转换序列。
解题思路:
1、利用广度优先遍历bfs找出最短路径,由上一篇广度优先可得,总共5层,然后记录好每层出现的单词(这里记录的时候要考虑重复问题)
2、然后通过「回溯算法(深度优先遍历dfs)」得到所有的最短路径。
import queue
def bfs(begin_word, end_word, word_list):
"""
由图分析可知找出 {'hit': ['hot'], 'hot': ['dot', 'lot'], 'dot': ['dog'],
这种格式的字典: 'lot': ['log'], 'dog': ['cog'], 'log': ['cog']}
:param begin_word:
:param end_word:
:param word_list:
:return:
"""
if end_word not in word_list:
return []
word_dict = dict()
for word in word_list:
for i in range(len(word)):
key = "{0}*{1}".format(word[:i], word[i+1:])
word_dict[key] = word_dict.get(key, []) + [word]
# 利用hash(字典)记录:key(当前节点单词),value(当前节点的子节点,用列表存放)
# 往value添加的值不能是上一节点的value
success_dict = dict()
queue_data = queue.Queue()
queue_data.put(begin_word)
# 存放当前节点
visited = set()
# 存放当前节点的子节点,要及时更新放进visited(子节点也是下一层的key)
next_level_word = set()
while not queue_data.empty():
current_word = queue_data.get()
for i in range(len(current_word)):
# 寻找当前节点(key)的所有子节点(value)
s = "{0}*{1}".format(current_word[:i], current_word[i + 1:])
for child_word in word_dict.get(s, []):
# 如果当前子节点不是上一个key的子节点,说明没被访问过,更新变量
if child_word not in visited or child_word == end_word:
if child_word != end_word:
# 最后一个节点没有子节点,不用加到队列中
queue_data.put(child_word)
next_level_word.add(child_word)
success_dict[current_word] = success_dict.get(current_word, []) + [child_word]
# 取两集合全部的元素(并集,等价于将 next_level_word 里的所有元素添加到 visited 中)
# 实际上这里也可以不用next_level_word这个标记,上面直接add到visited中而不用add到next_level_word然后再这里取并集
# 多next_level_word这个变量为了更好理解
visited |= next_level_word
next_level_word.clear()
return success_dict
def dfs(begin_word, end_word, success_dic, path, result):
"""
深度优先遍历(回溯法)找到所有最短路径
:param result: 保存结果值
:param path: 挑选最短路径中的单词,充当stack的作用 进栈(append),出栈(pop)
:param success_dic:
:param begin_word:
:param end_word:
:return:
"""
# path 和 result 不能在这里定义,不然每次执行函数都赋空了
# path = list()
# result = list()
# 找到程序结束点即当找到一条最短路径后沿着原路再返回,返回到中间可能得分叉口再继续找下去
if begin_word == end_word:
# 不能result=path或者result.append(path),
# 这样赋值path变化result也会跟着变化,和浅拷贝有点像
result.append(path[:])
return
success_word = success_dic[begin_word]
for child in success_word:
path.append(child)
dfs(child, end_word, success_dic, path, result)
path.pop()
# 要不要都行,因为程序到这里已经执行完了,反正result最后的值就是想要的结果
return result
success = bfs('hit', 'cog', ["hot", "dot", "dog", "lot", "log", "cog"])
short_path = dfs('hit', 'cog', success, ['hit'], [])
回溯VS递归
回溯法从问题本身出发,寻找可能实现的所有情况。和穷举法的思想相近,不同在于穷举法是将所有的情况都列举出来以后再一一筛选,而回溯法在列举过程如果发现当前情况根本不可能存在,就停止后续的所有工作,返回上一步进行新的尝试。
递归是从问题的结果出发,例如求 n!,要想知道 n!的结果,就需要知道 n*(n-1)! 的结果,而要想知道 (n-1)! 结果,就需要提前知道 (n-1)*(n-2)!。这样不断地向自己提问,不断地调用自己的思想就是递归。
def factorial(num):
if num == 1:
return 1
else:
# return num * factorial(num-1)
result = factorial(num-1)
return num * result
# 下一个值依赖上一个值。所以才每次return上一个值
print(factorial(5))
这里我们不写成注释的那一句(是一样的效果), 以一个阶乘的程序列一下递归的详细过程:
每一个绿框依次编号为1-5:每一个绿框我们可以理解为一个生命周期(即一次完整的函数执行过程),程序先是依次从1-5开始执行,到5的时候有返回值了(return是函数的终止点),返回到4,4往下面继续执行语句 return 2 * 1,即返回2,继续到3,往下面执行return 3 * 2,依次类推。这里每个函数都有一个返回值,即result的结果每次都是一个数字。
再回过头来看回溯VS递归:
上面的阶乘(典型的递归算法),每次都有一个return 作为函数结尾语句,这里是因为想要得到当前的结果就得知道上一次的结果,所以就每次把结果值返回(当然每次结果值如果没有联系也可以不必返回,但是总体必须有一个return作为函数结束点)。
单词接龙这道题(回溯),注意这里的return的用法和位置,这里不可以在path.pop()语句后面增加一句return,这里是在for语句中,如果加,只能得到一个结果,得不到完整结果。直接加return好像理解即返回上一次函数执行的地方,当然这里没有加return也会执行上一次函数执行的地方,直到生命周期(函数)都执行完毕为止。这样理解没错,但是当for语句字典success_word有多个值即走到路径分支的时候本来应该继续往另一条分支执行,你直接return了,所以他就往原来分支一路返回了,所以只能得到一条结果。所以这里注意一下return的使用位置。我这里加在了最后是因为程序执行完了,当然如果打印会有很多个结果,因为会执行多轮。
结论:
回溯和递归唯一的联系就是,回溯法可以用递归思想实现。