單詞接龍---快速建圖----雙向BFS(廣度優先遍歷)
轉載(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)
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
相關文章
- 廣度優先遍歷圖解圖解
- js實現深度優先遍歷和廣度優先遍歷JS
- python實現圖(基於圖的不同儲存方式)的深度優先(DFS)和廣度(BFS)優先遍歷Python
- LeetCode題解:127. 單詞接龍,雙向BFS,JavaScript,詳細註釋LeetCodeJavaScript
- 圖的遍歷:深度優先搜尋與廣度優先搜尋
- 深度優先遍歷,廣度優先遍歷實現物件的深拷貝物件
- bfs廣度優先搜尋
- 寬度優先遍歷
- 【PTA】鄰接矩陣儲存圖的深度優先遍歷矩陣
- 圖論系列之「廣度優先遍歷及無權圖的最短路徑(ShortPath)」圖論
- 【演算法】廣度/寬度優先搜尋(BFS)演算法
- python 實現二叉樹的深度&&廣度優先遍歷Python二叉樹
- 演算法競賽——BFS廣度優先搜尋演算法
- 廣度優先搜尋(BFS)思路及演算法分析演算法
- 基本演算法——深度優先搜尋(DFS)和廣度優先搜尋(BFS)演算法
- 速記圖的遍歷(DFS和BFS)
- POJ1915,雙向寬度優先搜尋
- 十、深度優先 && 廣度優先
- 深度優先與廣度優先
- 【LeetCode】127. 單詞接龍LeetCode
- P1019 單詞接龍(dfs)
- Node中的兩種遍歷方式-深度優先和廣度優先(附Node刪除檔案例子進行詳解)
- 圖論系列之「深度優先遍歷及聯通分量」圖論
- leetcode 127. 單詞接龍(C++)LeetCodeC++
- 資料結構-樹以及深度、廣度優先遍歷(遞迴和非遞迴,python實現)資料結構遞迴Python
- 圖的廣度優先搜尋和深度優先搜尋Python實現Python
- ybtoj:廣度優先搜尋
- (BFS廣度優先演算法) 油田問題演算法
- 廣度優先與深度優先類似嗎? - Mario
- L2-006 樹的遍歷(BFS)
- 用python深度優先遍歷解迷宮問題Python
- Python 圖_系列之基於鄰接炬陣實現廣度、深度優先路徑搜尋演算法Python演算法
- 圖論系列之「基於深度優先遍歷的尋路演算法 (Path) 」圖論演算法
- 圖的深度遍歷(C語言)鄰接矩陣表示C語言矩陣
- CF995E Number Clicker (雙向BFS)
- 演算法(三):圖解廣度優先搜尋演算法演算法圖解
- 雙向迴圈連結串列————遍歷、查詢、插入結點
- 非遞迴實現先序遍歷和中序遍歷遞迴