LeetCode[BFS]----Word Ladder

Given two words (beginWord and endWord), and a dictionary's word list, find the length of shortest transformation sequence from beginWord to endWord, such that:

  1. Only one letter can be changed at a time.
  2. Each transformed word must exist in the word list. Note that beginWord is not a transformed word.

Note:

  • Return 0 if there is no such transformation sequence.
  • All words have the same length.
  • All words contain only lowercase alphabetic characters.
  • You may assume no duplicates in the word list.
  • You may assume beginWord and endWord are non-empty and are not the same.

Example 1:

Input:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]

Output: 5

Explanation: As one shortest transformation is "hit" -> "hot" -> "dot" -> "dog" -> "cog",
return its length 5.

Example 2:

Input:
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log"]

Output: 0

Explanation: The endWord "cog" is not in wordList, therefore no possible transformation.

分析:

题目意思是给定一个不重复的、元素长度相同的序列,给定起始两个单词,要求不重复地利用序列中的单词,进行初始词到结束词的最短路径。

这题蛮有意思的,整个做题过程中前前后后改了很多遍,不断超时超时又超时。。

拿到题目后我认为这是一道搜索问题,先是写了个程序进行dfs深度优先搜索,大意是每次从列表中选择一个一个单词,该单词作为新的初始词(beginWord),在剩余的列表中寻找该初始词到结束词的最短路径,在递归的过程中记录路径长度。

然而这种方式效率实在太低,通过几个case后就超时了,迫使我继续思考。后来想到,在上述递归的过程中,存在大量重复的搜索。比如某个列表[A, B, C, D],程序会分别判断列表中每个词到结束词的最短路径,但是这个过程中程序可能多次判断A到结束词的最短路径,B到结束词的最短路径等,于是我用一个列表来记录当前已经访问过的点,如果这个点确定到不了结束点,则标记下,后续不再访问该节点,效率提高了很多,但是仍然超时。。

再之后,想到用列表来记录已经访问过节点,如果一个节点已经被访问过,那么在下一次迭代中将不再访问。思路是,从结束词出发,遍历列表,如果有词能达到结束词,那么最短路径中必然包含这些结束词(中的一个或多个),之后在下一轮的迭代中,程序将排除掉这些结束词,继续以新的词为结束词,重复上述过程,直到某个词等于开始词,迭代结束,最短路径也被算出返回。举个例子,假如某个列表[A, B, C, D, E, F, G], 到达结束词EndWord 的词有[A B C],那么如果这个列表有最短路径,那么这些结束词最后必须要经过[A B C]中的某一个到达结束词,并且这些词[A B C]只会在这一层被使用,于是在下一层迭代时,可以以[A B C]为新的结束词,在删减后的列表[D, E, F, G]中寻找最短路径。这本质上是一种BFS的方法。

代码如下(注意,仍然超时):

class Solution:
    def ladderLength(self, beginWord, endWord, wordList):
        """
        :type beginWord: str
        :type endWord: str
        :type wordList: List[str]
        :rtype: int
        """
        cur_path = 0

        if beginWord in wordList:
            wordList.remove(beginWord)

        if endWord not in wordList:
            return 0

        if endWord in wordList:
            wordList.remove(endWord)
        return self.dfs(beginWord, set([endWord]), set(wordList), cur_path)

    def dfs(self, beginWord, endWordSet, wordSet, cur_path):
        '''递归寻找最短路径'''
        for ew in endWordSet:  # 以endWordSet为新的结束词,来寻找这些结束词到开始词的最短路径
            if self.can_change(beginWord, ew):  # 如果这些词就已经能导到开始词了,则最短路径已经找到,返回
                return cur_path + 2

        cur_word_set = set()
        should_recursion = False
        for w in wordSet: # 1 对于列表中每个词
            for ew in endWordSet: # 2 对于新的结束词列表中的每个词
                if self.can_change(w, ew): # 3 判断是否能够转换
                    cur_word_set.add(w)  # 4 生成新的结束词,作为下一轮的结果
                    should_recursion = True
        if should_recursion:
            return self.dfs(beginWord, cur_word_set, wordSet - cur_word_set, cur_path + 1)
        return 0

    def can_change(self, word1, word2):
        '''两个单词之间作比较,看是否满足两个单词之间只有一个字符不同的设定'''
        diff_words = 0
        for i, v in enumerate(word1):
            if word1[i] != word2[i]:
                diff_words += 1
                if diff_words > 1:
                    return False
        if diff_words == 1:
            return True
        return False

上述代码依旧超时,虽然每次递归单词列表总量都在减少,但是在单词列表总长度很大时依然会超时。上述代码耗时主要提现在注释中1 和 2的双重循环中。

但是到了这个过程我已经不知道怎么去优化了,看了下LeetCode讨论区的代码,发现大家并没有使用我这种遍历判断的方法,而是按位对单词进行修改,判断修改后单词是否在单词列表中。了解到这层之后,用这种方法替换了我的双重循环,然后终于AC了。

代码如下(已AC):

class Solution:
    def ladderLength(self, beginWord, endWord, wordList):
        """
        :type beginWord: str
        :type endWord: str
        :type wordList: List[str]
        :rtype: int
        """
        wordSet = set(wordList)
        if beginWord in wordSet:
            wordSet.remove(beginWord)

        if endWord in wordSet:
            wordSet.remove(endWord)
        else:
            return 0

        endWordSet = set([endWord])
        return self.dfs(beginWord, endWordSet, wordSet - endWordSet, cur_path=0)

    def dfs(self, beginWord, endWordSet, wordSet, cur_path):
        if len(endWordSet) == 0:
            return 0
        cur_word_set = set()
        for ew in endWordSet:  # 1. 对于列表中的每个单词
            for i in range(len(beginWord)):  # 2. 对于单词的每一位
                for l in 'abcdefghijklmnopqrstuvwxyz':  # 3. 对于每一位可以替换的 26 个字母
                    c_w = ew[: i] + l + ew[i + 1:]
                    if c_w in wordSet:
                        cur_word_set.add(c_w)
                    if c_w == beginWord:
                        return cur_path + 2
        return self.dfs(beginWord, cur_word_set, wordSet - cur_word_set, cur_path + 1)

上述代码最耗时的地方在注释的1.2.3出,但是其每次循环的次数固定,不过len(结束词列表长度) * 单词长度 * 26;而原先的双重循环,则是len(结束词列表长度) * len(总列表长度) ,新的判断方式能够极大地提高效率。

继续修改代码,将BFS的递归代码改成非递归方式。

代码如下:

class Solution:
    def ladderLength(self, beginWord, endWord, wordList):
        """
        :type beginWord: str
        :type endWord: str
        :type wordList: List[str]
        :rtype: int
        """
        wordSet = set(wordList)
        if endWord in wordSet:
            wordSet.remove(endWord)
        else:
            return 0

        endWordSet = set([endWord])
        path = 1
        while True:
            curWordSet = set()
            for ew in endWordSet:
                for i in range(len(beginWord)):
                    for l in 'abcdefghijklmnopqrstuvwxyz':
                        c_w = ew[: i] + l + ew[i + 1:]
                        if c_w in wordSet:
                            curWordSet.add(c_w)
                        if c_w == beginWord:
                            return path + 1
            endWordSet = curWordSet
            wordSet = wordSet - curWordSet
            if len(endWordSet) == 0:
                return 0
            path += 1
        return 0

再之后,在上面代码基础上,进行修改,并解决了WordLadderII。

你可能感兴趣的:(Python,算法之美,LeetCode)