单词接龙II-(深度优先算法)

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的使用位置。我这里加在了最后是因为程序执行完了,当然如果打印会有很多个结果,因为会执行多轮。

结论:

回溯和递归唯一的联系就是,回溯法可以用递归思想实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值