單詞接龍---快速建圖----雙向BFS(廣度優先遍歷)

shixiaogan8008發表於2020-12-09

轉載(https://leetcode-cn.com/problems/word-ladder/solution/dan-ci-jie-long-by-leetcode-solution/)

方法一:廣度優先搜尋 + 優化建圖
思路

本題要求的是最短轉換序列的長度,看到最短首先想到的就是廣度優先搜尋。想到廣度優先搜尋自然而然的就能想到圖,但是本題並沒有直截了當的給出圖的模型,因此我們需要把它抽象成圖的模型。

我們可以把每個單詞都抽象為一個點,如果兩個單詞可以只改變一個字母進行轉換,那麼說明他們之間有一條雙向邊。因此我們只需要把滿足轉換條件的點相連,就形成了一張圖。

基於該圖,我們以 beginWord 為圖的起點,以 endWord 為終點進行廣度優先搜尋,尋找 beginWord 到 endWord 的最短路徑。
在這裡插入圖片描述
演算法

基於上面的思路我們考慮如何程式設計實現。

首先為了方便表示,我們先給每一個單詞標號,即給每個單詞分配一個 id。建立一個由單詞 word 到 id 對應的對映 wordId,並將 beginWord 與 wordList 中所有的單詞都加入這個對映中。之後我們檢查 endWord 是否在該對映內,若不存在,則輸入無解。我們可以使用雜湊表實現上面的對映關係。

然後我們需要建圖,依據樸素的思路,我們可以列舉每一對單詞的組合,判斷它們是否恰好相差一個字元,以判斷這兩個單詞對應的節點是否能夠相連。但是這樣效率太低,我們可以優化建圖。

具體地,我們可以建立虛擬節點。對於單詞 hit,我們建立三個虛擬節點 it、ht、hi*,並讓 hit 向這三個虛擬節點分別連一條邊即可。如果一個單詞能夠轉化為 hit,那麼該單詞必然會連線到這三個虛擬節點之一。對於每一個單詞,我們列舉它連線到的虛擬節點,把該單詞對應的 id 與這些虛擬節點對應的 id 相連即可。

最後我們將起點加入佇列開始廣度優先搜尋,當搜尋到終點時,我們就找到了最短路徑的長度。注意因為新增了虛擬節點,所以我們得到的距離為實際最短路徑長度的兩倍。同時我們並未計算起點對答案的貢獻,所以我們應當返回距離的一半再加一的結果。

class Solution {
public:
    unordered_map<string, int> wordId;
    vector<vector<int>> edge;
    int nodeNum = 0;

    void addWord(string& word) {
        if (!wordId.count(word)) {
            wordId[word] = nodeNum++;
            edge.emplace_back();
        }
    }

    void addEdge(string& word) {
        addWord(word);
        int id1 = wordId[word];
        for (char& it : word) {
            char tmp = it;
            it = '*';
            addWord(word);
            int id2 = wordId[word];
            edge[id1].push_back(id2);
            edge[id2].push_back(id1);
            it = tmp;
        }
    }

    int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
        for (string& word : wordList) {
            addEdge(word);
        }
        addEdge(beginWord);
        if (!wordId.count(endWord)) {
            return 0;
        }
        vector<int> dis(nodeNum, INT_MAX);
        int beginId = wordId[beginWord], endId = wordId[endWord];
        dis[beginId] = 0;

        queue<int> que;
        que.push(beginId);
        while (!que.empty()) {
            int x = que.front();
            que.pop();
            if (x == endId) {
                return dis[endId] / 2 + 1;
            }
            for (int& it : edge[x]) {
                if (dis[it] == INT_MAX) {
                    dis[it] = dis[x] + 1;
                    que.push(it);
                }
            }
        }
        return 0;
    }
};

複雜度分析

時間複雜度:O(N \times C^2)O(N×C
2
)。其中 NN 為 wordList 的長度,CC 為列表中單詞的平均長度。

建圖過程中,對於每一個單詞,我們需要列舉它連線到的所有虛擬節點,時間複雜度為 O©O©,將這些單詞加入到雜湊表中,時間複雜度為 O(N \times C)O(N×C),因此總時間複雜度為 O(N \times C)O(N×C)。

廣度優先搜尋的時間複雜度最壞情況下是 O(N \times C)O(N×C)。每一個單詞需要擴充出 O©O© 個虛擬節點,因此節點數 O(N \times C)O(N×C)。

空間複雜度:O(N \times C^2)O(N×C
2
)。其中 NN 為 wordList 的長度,CC 為列表中單詞的平均長度。雜湊表中包含 O(N \times C)O(N×C) 個節點,每個節點佔用空間 O©O©,因此總的空間複雜度為 O(N \times C^2)O(N×C
2
)。

方法二:雙向廣度優先搜尋
思路及解法

根據給定字典構造的圖可能會很大,而廣度優先搜尋的搜尋空間大小依賴於每層節點的分支數量。假如每個節點的分支數量相同,搜尋空間會隨著層數的增長指數級的增加。考慮一個簡單的二叉樹,每一層都是滿二叉樹的擴充套件,節點的數量會以 22 為底數呈指數增長。

如果使用兩個同時進行的廣搜可以有效地減少搜尋空間。一邊從 beginWord 開始,另一邊從 endWord 開始。我們每次從兩邊各擴充套件一層節點,當發現某一時刻兩邊都訪問過同一頂點時就停止搜尋。這就是雙向廣度優先搜尋,它可以可觀地減少搜尋空間大小,從而提高程式碼執行效率。
在這裡插入圖片描述

class Solution {
public:
    unordered_map<string, int> wordId;
    vector<vector<int>> edge;
    int nodeNum = 0;

    void addWord(string& word) {
        if (!wordId.count(word)) {
            wordId[word] = nodeNum++;
            edge.emplace_back();
        }
    }

    void addEdge(string& word) {
        addWord(word);
        int id1 = wordId[word];
        for (char& it : word) {
            char tmp = it;
            it = '*';
            addWord(word);
            int id2 = wordId[word];
            edge[id1].push_back(id2);
            edge[id2].push_back(id1);
            it = tmp;
        }
    }

    int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
        for (string& word : wordList) {
            addEdge(word);
        }
        addEdge(beginWord);
        if (!wordId.count(endWord)) {
            return 0;
        }

        vector<int> disBegin(nodeNum, INT_MAX);
        int beginId = wordId[beginWord];
        disBegin[beginId] = 0;
        queue<int> queBegin;
        queBegin.push(beginId);

        vector<int> disEnd(nodeNum, INT_MAX);
        int endId = wordId[endWord];
        disEnd[endId] = 0;
        queue<int> queEnd;
        queEnd.push(endId);

        while (!queBegin.empty() && !queEnd.empty()) {
            int queBeginSize = queBegin.size();
            for (int i = 0; i < queBeginSize; ++i) {
                int nodeBegin = queBegin.front();
                queBegin.pop();
                if (disEnd[nodeBegin] != INT_MAX) {
                    return (disBegin[nodeBegin] + disEnd[nodeBegin]) / 2 + 1;
                }
                for (int& it : edge[nodeBegin]) {
                    if (disBegin[it] == INT_MAX) {
                        disBegin[it] = disBegin[nodeBegin] + 1;
                        queBegin.push(it);
                    }
                }
            }

            int queEndSize = queEnd.size();
            for (int i = 0; i < queEndSize; ++i) {
                int nodeEnd = queEnd.front();
                queEnd.pop();
                if (disBegin[nodeEnd] != INT_MAX) {
                    return (disBegin[nodeEnd] + disEnd[nodeEnd]) / 2 + 1;
                }
                for (int& it : edge[nodeEnd]) {
                    if (disEnd[it] == INT_MAX) {
                        disEnd[it] = disEnd[nodeEnd] + 1;
                        queEnd.push(it);
                    }
                }
            }
        }
        return 0;
    }
};

複雜度分析

時間複雜度:O(N \times C^2)O(N×C
2
)。其中 NN 為 wordList 的長度,CC 為列表中單詞的平均長度。

建圖過程中,對於每一個單詞,我們需要列舉它連線到的所有虛擬節點,時間複雜度為 O©O©,將這些單詞加入到雜湊表中,時間複雜度為 O(N \times C)O(N×C),因此總時間複雜度為 O(N \times C)O(N×C)。

雙向廣度優先搜尋的時間複雜度最壞情況下是 O(N \times C)O(N×C)。每一個單詞需要擴充出 O©O© 個虛擬節點,因此節點數 O(N \times C)O(N×C)。

空間複雜度:O(N \times C^2)O(N×C
2
)。其中 NN 為 wordList 的長度,CC 為列表中單詞的平均長度。雜湊表中包含 O(N \times C)O(N×C) 個節點,每個節點佔用空間 O©O©,因此總的空間複雜度為 O(N \times C^2)O(N×C
2
)。

作者:LeetCode-Solution
連結:https://leetcode-cn.com/problems/word-ladder/solution/dan-ci-jie-long-by-leetcode-solution/
來源:力扣(LeetCode)
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

相關文章