【每日一题+经典】单词接龙II——通配符+bfs+dfs ++ 双向bfs

2020/06/07 返回搜索路径的BFS
【每日一题+经典】单词接龙II——通配符+bfs+dfs ++ 双向bfs_第1张图片

是一道典型的bfs题目,其实可以用回溯方法,但是复杂度太高。
难点在于如何降低复杂度。

首先是通配符的方法,构建了一个字典,键是通配符,对一个的值是所以符合这个通配符的单词。

其次是结合了bfs和dfs的方法,bfs保证了找到最短的路径,同时在bfs中构建了一个后继字典,这样在dfs中可以方便的得到路径。后继字典的构建保证了不会有绕远的路径出现,因为有visited数组。

from collections import defaultdict
from collections import deque
class Solution:
    def findLadders(self, beginWord, endWord, wordList):
        # 先将 wordList 放到哈希表里,便于判断某个单词是否在 wordList 里
        word_set = set(wordList)
        res = []
        if len(word_set) == 0 or endWord not in word_set:
            return res
        successors = defaultdict(set)
        # 构建通配符
        buckets = defaultdict(list)
        for word in wordList:
            for i in range(len(beginWord)):
                match = word[:i] + '_' + word[i+1:]
                buckets[match].append(word)
        
        # 第 1 步:使用广度优先遍历得到后继结点列表 successors
        # key:字符串,value:广度优先遍历过程中 key 的后继结点列表
        found = self.__bfs(beginWord, endWord, word_set, successors,buckets)

        if not found:
            return res
        # 第 2 步:基于后继结点列表 successors ,使用回溯算法得到所有最短路径列表        
        if beginWord not in successors:
            return
        path = [beginWord]    
        self.__dfs(beginWord, endWord, successors, path, res)
        return res

    def __bfs(self, beginWord, endWord, word_set, successors, buckets):
        queue = deque()
        queue.append(beginWord)

        visited = set()
        visited.add(beginWord)

        found = False
        word_len = len(beginWord)
        next_level_visited = set()

        while queue:
            current_size = len(queue)
            for i in range(current_size):
                current_word = queue.popleft()
                for j in range(word_len):
                    match = current_word[:j] + '_' + current_word[j+1:]
                    for next_word in buckets[match]: 
                        if next_word not in visited:
                            if next_word == endWord:
                                found = True
                            next_level_visited.add(next_word)
                            queue.append(next_word)
                            successors[current_word].add(next_word)
            if found:
                break
            # 取两集合全部的元素(并集,等价于将 next_level_visited 里的所有元素添加到 visited 里)
            visited |= next_level_visited
            next_level_visited.clear()
        return found

    def __dfs(self, beginWord, endWord, successors, path, res):
        if beginWord == endWord:
            res.append(path[:])
            return
        successor_words = successors[beginWord]
        for next_word in successor_words:
            path.append(next_word)
            self.__dfs(next_word, endWord, successors, path, res)
            path.pop()

如果觉着这个不够过瘾,还能更酷炫的双向BFS。维护两个邻居集合,begin_visitedend_visited。只要找到重合的,就是是找到了最短的路径。需要注意的是后驱字典的更新,与查找的方向有关。

class Solution(object):
    def findLadders(self, beginWord, endWord, wordList):
        # 先将 wordList 放到哈希表里,便于判断某个单词是否在 wordList 里
        word_set = set(wordList)
        res = []
        if len(word_set) == 0 or endWord not in word_set:
            return res

        dic = collections.defaultdict(set)
        for word in wordList:
            for i in range(len(beginWord)):
                match = word[:i]+'.'+word[i+1:]
                dic[match].add(word)

        successors = defaultdict(set)
        # 第 1 步:使用广度优先遍历得到后继结点列表 successors
        # key:字符串,value:广度优先遍历过程中 key 的后继结点列表
        found = self.__bidirectional_bfs(beginWord, endWord, word_set, successors, dic)
        if not found:
            return res
        # 第 2 步:基于后继结点列表 successors ,使用回溯算法得到所有最短路径列表
        path = [beginWord]
        self.__dfs(beginWord, endWord, successors, path, res)
        return res

    def __bidirectional_bfs(self, beginWord, endWord, word_set, successors, dic):
        visited = set()
        visited.add(beginWord)
        visited.add(endWord)

        begin_visited = set()
        begin_visited.add(beginWord)

        end_visited = set()
        end_visited.add(endWord)

        found = False
        forward = True
        word_len = len(beginWord)
        while begin_visited:
            # 从小的开始进行,begin_visited和end_visited都是需要寻找的邻居的节点集合
            if len(begin_visited) > len(end_visited):
                begin_visited, end_visited = end_visited, begin_visited
                forward = not forward

            next_level_visited = set()
            for current_word in begin_visited:
                for j in range(word_len):
                    match = current_word[:j]+'.'+current_word[j+1:]
                    for next_word in dic[match]:
                        if next_word in end_visited:
                            found = True
                            # 在另一侧找到单词以后,还需把这一层关系添加到「后继结点列表」
                            self.__add_to_successors(successors, forward, current_word, next_word)
                        if next_word not in visited:
                            next_level_visited.add(next_word)
                            self.__add_to_successors(successors, forward, current_word, next_word)
            ## !!!这里是关键,维护了begin,是新的需要寻找邻居的节点
            begin_visited = next_level_visited
            # 取两集合全部的元素(并集,等价于将 next_level_visited 里的所有元素添加到 visited 里)
            visited |= next_level_visited
            if found:
                break
        return found

    def __add_to_successors(self, successors, forward, current_word, next_word):
        if forward:
            successors[current_word].add(next_word)
        else:
            successors[next_word].add(current_word)

    def __dfs(self, beginWord, endWord, successors, path, res):
        if beginWord == endWord:
            res.append(path[:])
            return

        if beginWord not in successors:
            return

        successor_words = successors[beginWord]
        for next_word in successor_words:
            path.append(next_word)
            self.__dfs(next_word, endWord, successors, path, res)
            path.pop()

你可能感兴趣的:(每日一题,leetcode经典题目)