1. 程式人生 > >leetcode做題日記【單詞接龍】:一個程式設計小菜雞寫出的程式碼能有多菜

leetcode做題日記【單詞接龍】:一個程式設計小菜雞寫出的程式碼能有多菜

leetcode第47題:單詞接龍

題幹如下:

單詞接龍

給定兩個單詞(beginWord endWord)和一個字典,找到從 beginWord 到 endWord 的最短轉換序列的長度。轉換需遵循如下規則:

  1. 每次轉換隻能改變一個字母。
  2. 轉換過程中的中間單詞必須是字典中的單詞。

說明:

  • 如果不存在這樣的轉換序列,返回 0。
  • 所有單詞具有相同的長度。
  • 所有單詞只由小寫字母組成。
  • 字典中不存在重複的單詞。
  • 你可以假設 beginWordendWord 是非空的,且二者不相同。
  • 示例 1:

    輸入:
    beginWord = "hit",
    endWord = "cog",
    wordList = ["hot","dot","dog","lot","log","cog"]
    
    輸出: 5
    
    解釋: 一個最短轉換序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog",
         返回它的長度 5。
    

    示例 2:

    輸入:
    beginWord = "hit"
    endWord = "cog"
    wordList = ["hot","dot","dog","lot","log"]
    
    輸出: 0
    
    解釋: endWord "cog" 不在字典中,所以無法進行轉換

 因為這道題出現在高階演算法的【樹和圖】模組,同時要求的是最短轉換序列的長度,就會很自然地想到圖中的最小路徑演算法。初步考慮是使用廣度優先遍歷,由於是廣度優先遍歷,遍歷到的第一個結果就是最短序列的長度。由於從一開始建立一個完整的圖是一件非常耗時的事情,參考了一些內容後選擇用一個佇列(deque)來儲存當前遍歷到的單詞,對單詞中的每個字母做替換並判斷是否在字典中,如果遍歷完了所有的變換形式依然沒有找到能夠到達最終結果的路徑,就返回0。

from collections import deque
import string

class Solution(object):
    def ladderLength(self, beginWord, endWord, wordList):
        """
        :type beginWord: str
        :type endWord: str
        :type wordList: List[str]
        :rtype: int
        """
        #排除那些顯而易見的錯誤情況
        if endWord not in wordList:
            return 0
        #避免形成環
        if beginWord in wordList:
            wordList.remove(beginWord)
        #set的查詢時間是常數級的
        words = set(wordList)
        # for word in wordList:
        #     words.add(word)
        #下面開始廣度優先遍歷
        return self.bfs(words,beginWord,endWord)
    
    def bfs(self,words,beginWord,endWord):
        #由於需要先進先出,採用deque雙向佇列模組
        current = deque()
        current.append([beginWord,1])
        while current:
            #print(current)
            [word,step] = current.popleft()
            #查詢成功
            if word == endWord:
                return step
            #下一個單詞的可達序列
            nextWord = self.next(word,words)
            for ele in nextWord:
                current.append([ele,step + 1])
        #最後都沒有找到
        return 0
    
    def next(self,word,words):
        result = []
        letters = string.ascii_lowercase
        for i in range(len(word)):
            for letter in letters:
                if word[i] != letter:
                    new_word = list(word)
                    new_word[i] = letter
                    new_word = "".join(new_word)
                    if new_word in words:
                        result.append(new_word)
                        #避免形成環
                        words.remove(new_word)
        return result

這個思路非常自然,然後執行也通過了,我高高興興地點了“提交”,執行的圈圈轉了好久,我得到了這個結果:

 ……

我知道這是一個非常爛的程式碼,但這波衝擊有點大,當時的我是這樣的:

失……失敗是成功他媽……

但是講道理的話,我大致百度了一下別人家的程式碼,基本思路也是廣度優先遍歷,所以問題在哪裡呢?哭泣中的我點開了排名第一的程式碼。【下面的程式碼來自於對leetcode47題上排名的第一程式碼,侵刪】



class Solution(object):
    def ladderLength(self, beginWord, endWord, wordList):
        if endWord not in wordList:
            return 0

        wordList = set(wordList)
        forward, backward, n, cnt = {beginWord}, {endWord}, len(beginWord), 2
        dic = set(string.ascii_lowercase)

        while len(forward) > 0 and len(backward) > 0:
            if len(forward) > len(backward): # 加速
                forward, backward = backward, forward

            next = set()
            for word in forward:
                for i, char in enumerate(word):
                    first, second = word[:i], word[i + 1:]
                    for c in dic: # 遍歷26個字母
                        candidate = first + c + second
                        if candidate in backward: # 如果找到了,返回結果,沒有找到,則在wordList中繼續尋找
                            return cnt

                        if candidate in wordList:
                            wordList.discard(candidate) # 從wordList中去掉單詞
                            next.add(candidate) #加入下一輪的bfs中
            forward = next
            cnt += 1
        return 0

總體思路是bfs沒錯,現在看一下程式碼的實現細節。

(1)如何對單詞單次修改一個字母

眾所周知,字串本身是不可變的,因此對單詞做修改必須新建一個字串。我記得有這麼一個join方法,就拿過來做了字串——列表——字串的轉換。對比我的程式碼和優秀的程式碼,人家在這一點上可能做得比我好。

於是我把

                    new_word = list(word)
                    new_word[i] = letter
                    new_word = "".join(new_word)

改成了

                new_word = word[:i] + letter + word[i + 1:]

效果立竿見影,執行結果竄到了30%。之前在寫別的程式碼時,曾發現用+來連線字串還挺快的,但是切片是一種比較慢的操作,因此沒有采用這種方法,但仔細想想字串轉列表再join確實更麻煩啊……

我覺得我還可以在搶救一下……

(2)同時取出陣列中的索引和元素

我是這樣寫的:

            for i in range(len(word)):
                char = word[i]

看了別人的程式碼之後我改成了這樣:

            for i, char in enumerate(word):

其他的地方都沒有改,重新執行,48%。

嗯……自己真的經驗太淺,這些還是需要積累。

(3)這個時候我發現分兩個函數出來其實沒有什麼必要,而且我還把

letters = set(string.ascii_lowercase)

這句話運行了好多遍,所以把原來的函式合併到一塊裡面,並把這句話提前到前邊,改完的程式碼如下:

class Solution(object):
    def ladderLength(self, beginWord, endWord, wordList):
        """
        :type beginWord: str
        :type endWord: str
        :type wordList: List[str]
        :rtype: int
        """
        #排除那些顯而易見的錯誤情況
        if endWord not in wordList:
            return 0
        #避免形成環
        if beginWord in wordList:
            wordList.remove(beginWord)
        letters = set(string.ascii_lowercase)
        words = set(wordList)
        #下面開始廣度優先遍歷
        current = deque()
        current.append([beginWord,1])
        while current:
            #print(current)
            [word,step] = current.popleft()
            #查詢成功
            if word == endWord:
                return step
            #下一個單詞的可達序列
            nextWord = []
            #for i in range(len(word)):
            for i, char in enumerate(word):
                for letter in letters:
                    new_word = word[:i] + letter + word[i + 1:]
                    if new_word == endWord:
                        return step + 1
                    if new_word in words:
                        nextWord.append(new_word)
                        #避免形成環
                        words.remove(new_word)            
            for ele in nextWord:
                current.append([ele,step + 1])                
        #最後都沒有找到
        return 0

55%。

從第一個對於切片的修改開始已經提高了不少,很快就可以及格了。說到切片,上面的程式碼裡,好像一邊在擔心切片用時間長,一邊做了好多次切片……

我差不多想掐死自己了。

所以把

                    for letter in letters:
                        new_word = word[:i] + letter + word[i + 1:]

改成了

                    first, second = word[:i], word[i + 1:]
                    for c in letters: 
                        new_word = first + c + second 

提交後69%。所以不是人家的題難,真的是自己不行……

(4)最後這個把成績提升到100%的點,是一個我覺得非常巧妙的點。在我的原始碼中,廣度優先遍歷的方式是依次從佇列中取數、加數。比方說,在某一個時間點,佇列中的元素很可能是這樣的:

[
    ['hot',2],
    ['hag',3],
    ['hap',3]
    ['cog',4]
]

字元是我胡亂編的。想說明的問題是,佇列中可以同時存在不同深度的點。而排名第一的答案它的遍歷模式是不同的,它先把某一深度的點遍歷完畢之後,才開始遍歷下一深度的點。

        while len(forward) > 0 and len(backward) > 0:
            if len(forward) > len(backward): # 加速
                forward, backward = backward, forward

            next = set()
            for word in forward:
                for i, char in enumerate(word):
                    first, second = word[:i], word[i + 1:]
                    for c in dic: # 遍歷26個字母
                        candidate = first + c + second
                        if candidate in backward: # 如果找到了,返回結果,沒有找到,則在wordList中繼續尋找
                            return cnt

                        if candidate in wordList:
                            wordList.discard(candidate) # 從wordList中去掉單詞
                            next.add(candidate) #加入下一輪的bfs中
            forward = next
            cnt += 1

這樣它就有一個非常巧妙的地方,也就是這兩句:


            if len(forward) > len(backward): # 加速
                forward, backward = backward, forward

backward這個集合一開始加入了endWord,之後沒有進行賦值操作,對它的操作就是在forward中互換,因此,forward和backward這兩個集合中一定有一個集合只包括一個元素。而我們要建立的是兩個集合之間元素的路徑關係【雖然其中有一個只有一個元素】,由於題目要求的結果只是一個路徑長度,從forward到backward,與從backward到forward的路徑長度,當然是一樣的。所以,我們可以對它們進行互換而不影響結果。

為什麼要進行互換呢?因為我們要對forward進行遍歷。

真的太牛逼了。

最後修改完的程式碼如下(已經改的跟人家第一的程式碼差不多了……)

class Solution(object):
    def ladderLength(self, beginWord, endWord, wordList):
        """
        :type beginWord: str
        :type endWord: str
        :type wordList: List[str]
        :rtype: int
        """
        if endWord not in wordList:
            return 0
        if beginWord in wordList:
            wordList.remove(beginWord)
        letters = set(string.ascii_lowercase)
        wordList = set(wordList)
        forward,backward,cnt = {beginWord},{endWord},2
        while len(forward) > 0 and len(backward) > 0:
            if len(forward) > len(backward):
                forward,backward = backward,forward
            
            current = set()
            for word in forward:
                for i, char in enumerate(word):                    
                    first, second = word[:i], word[i + 1:]
                    for c in letters: # 遍歷26個字母
                        new_word = first + c + second 
                        if new_word in backward:
                            return cnt
                        if new_word in wordList:
                            current.add(new_word)
                            wordList.remove(new_word)
            forward = current
            cnt += 1
        return 0

果然結果也是極好的。

從開始動筆寫這個程式碼,到最後改到一個差不多的結果,花了一個下午+半個晚上的時間,中間因為不知道導致自己程式執行慢的到底是哪些程式碼而進行了一些無用的修改。感覺在碼農這條路上自己可能連起步都還沒有做到,畢竟經驗太淺,技巧不夠,知識面也非常窄,程式碼各種冗餘有用的模組也不會用。

路漫漫其修遠兮吧。作為程式設計小白的見解可能也有諸多不足和錯誤,希望自己可以多多學習。