其他LeetCode題目歡迎訪問:LeetCode結題報告索引
Given two words (start and end), and a dictionary, find the length of shortest transformation sequence from start to end, such that:
- Only one letter can be changed at a time
- Each intermediate word must exist in the dictionary
For example,
Given:
start = "hit"
end = "cog"
dict = ["hot","dot","dog","lot","log"]
As one shortest transformation is "hit" -> "hot" -> "dot" -> "dog" -> "cog"
,
return its length 5
.
Note:
- Return 0 if there is no such transformation sequence.
- All words have the same length.
- All words contain only lowercase alphabetic characters.
分析:這種題,肯定是每次改變單詞的一個字母,然后逐漸搜索,很多人一開始就想到用dfs,其實像這種求最短路徑、樹最小深度問題bfs最適合,可以參考我的這篇博客bfs(層序遍歷)求二叉樹的最小深度。本題bfs要注意的問題:
- 和當前單詞相鄰的單詞是:對當前單詞改變一個字母且在字典中存在的單詞
- 找到一個單詞的相鄰單詞,加入bfs隊列后,要從字典中刪除,因為不刪除的話會造成類似於hog->hot->hog的死循環。而刪除對求最短路徑沒有影響,因為我們第一次找到該單詞肯定是最短路徑,即使后面其他單詞也可能轉化得到它,路徑肯定不會比當前的路徑短(如果要輸出所有最短路徑,則不能立即從字典中刪除,具體見下一題)
- bfs隊列中用NULL來標識層與層的間隔,每次碰到層的結尾,遍歷深度+1
我們利用和求二叉樹最小深度層序遍歷的方法來進行bfs,代碼如下: 本文地址
1 class Solution { 2 public: 3 int ladderLength(string start, string end, unordered_set<string> &dict) { 4 // IMPORTANT: Please reset any member data you declared, as 5 // the same Solution instance will be reused for each test case. 6 //BFS遍歷找到的第一個匹配就是最短轉換,空字符串是層與層之間的分隔標志 7 queue<string> Q; 8 Q.push(start); Q.push(""); 9 int res = 1; 10 while(Q.empty() == false) 11 { 12 string str = Q.front(); 13 Q.pop(); 14 if(str != "") 15 { 16 int strLen = str.length(); 17 for(int i = 0; i < strLen; i++) 18 { 19 char tmp = str[i]; 20 for(char c = 'a'; c <= 'z'; c++) 21 { 22 if(c == tmp)continue; 23 str[i] = c; 24 if(str == end)return res+1; 25 if(dict.find(str) != dict.end()) 26 { 27 Q.push(str); 28 dict.erase(str); 29 } 30 } 31 str[i] = tmp; 32 } 33 } 34 else if(Q.empty() == false) 35 {//到達當前層的結尾,並且不是最后一層的結尾 36 res++; 37 Q.push(""); 38 } 39 } 40 return 0; 41 } 42 };
Given two words (start and end), and a dictionary, find all shortest transformation sequence(s) from start to end, such that:
- Only one letter can be changed at a time
- Each intermediate word must exist in the dictionary
For example,
Given:
start = "hit"
end = "cog"
dict = ["hot","dot","dog","lot","log"]
Return
[ ["hit","hot","dot","dog","cog"], ["hit","hot","lot","log","cog"] ]
Note:
- All words have the same length.
- All words contain only lowercase alphabetic characters.
分析:本題主要的框架和上一題是一樣,但是還要解決兩個額外的問題:一、 怎樣保證求得所有的最短路徑;二、 怎樣構造這些路徑
第一問題:
- 不能像上一題第二點注意那樣,找到一個單詞相鄰的單詞后就立馬把它從字典里刪除,因為當前層還有其他單詞可能和該單詞是相鄰的,這也是一條最短路徑,比如hot->hog->dog->dig和hot->dot->dog->dig,找到hog的相鄰dog后不能立馬刪除,因為和hog同一層的單詞dot的相鄰也是dog,兩者均是一條最短路徑。但是為了避免進入死循環,再hog、dot這一層的單詞便利完成后dog還是得從字典中刪除。即等到當前層所有單詞遍歷完后,和他們相鄰且在字典中的單詞要從字典中刪除。
- 如果像上面那樣沒有立馬刪除相鄰單詞,就有可能把同一個單詞加入bfs隊列中,這樣就會有很多的重復計算(比如上面例子提到的dog就會被2次加入隊列)。因此我們用一個哈希表來保證加入隊列中的單詞不會重復,哈希表在每一層遍歷完清空(代碼中hashtable)。
- 當某一層的某個單詞轉換可以得到end單詞時,表示已經找到一條最短路徑,那么該單詞的其他轉換就可以跳過。並且遍歷完這一層以后就可以跳出循環,因為再往下遍歷,肯定會超過最短路徑長度
第二個問題:
- 為了輸出最短路徑,我們就要在比bfs的過程中保存好前驅節點,比如單詞hog通過一次變換可以得到hot,那么hot的前驅節點就包含hog,每個單詞的前驅節點有可能不止一個,那么每個單詞就需要一個數組來保存前驅節點。為了快速查找因此我們使用哈希表來保存所有單詞的前驅路徑,哈希表的key是單詞,value是單詞數組。(代碼中的unordered_map<string,vector<string> >prePath)
- 有了上面的前驅路徑,可以從目標單詞開始遞歸的構造所有最短路徑(代碼中的函數 ConstructResult)
1 class Solution { 2 public: 3 typedef unordered_set<string>::iterator HashIter; 4 vector<vector<string>> findLadders(string start, string end, unordered_set<string> &dict) { 5 // Note: The Solution object is instantiated only once and is reused by each test case. 6 queue<string> Q; 7 Q.push(start); Q.push(""); 8 bool hasFound = false; 9 unordered_map<string,vector<string> >prePath;//前驅路徑 10 unordered_set<string> hashtable;//保證bfs時插入隊列的元素不存在重復 11 while(Q.empty() == false) 12 { 13 string str = Q.front(), strCopy = str; 14 Q.pop(); 15 if(str != "") 16 { 17 int strLen = str.length(); 18 for(int i = 0; i < strLen; i++) 19 { 20 char tmp = str[i]; 21 for(char c = 'a'; c <= 'z'; c++) 22 { 23 if(c == tmp)continue; 24 str[i] = c; 25 if(str == end) 26 { 27 hasFound = true; 28 prePath[end].push_back(strCopy); 29 //找到了一條最短路徑,當前單詞的其它轉換就沒必要 30 goto END; 31 } 32 if(dict.find(str) != dict.end()) 33 { 34 prePath[str].push_back(strCopy); 35 //保證bfs時插入隊列的元素不存在重復 36 if(hashtable.find(str) == hashtable.end()) 37 {Q.push(str); hashtable.insert(str);} 38 } 39 } 40 str[i] = tmp; 41 } 42 } 43 else if(Q.empty() == false)//到當前層的結尾,且不是最后一層的結尾 44 { 45 if(hasFound)break; 46 //避免進入死循環,把bfs上一層插入隊列的元素從字典中刪除 47 for(HashIter ite = hashtable.begin(); ite != hashtable.end(); ite++) 48 dict.erase(*ite); 49 hashtable.clear(); 50 Q.push(""); 51 } 52 END: ; 53 } 54 vector<vector<string> > res; 55 if(prePath.find(end) == prePath.end())return res; 56 vector<string> tmpres; 57 tmpres.push_back(end); 58 ConstructResult(prePath, res, tmpres, start, end); 59 return res; 60 } 61 62 private: 63 //從前驅路徑中回溯構造path 64 void ConstructResult(unordered_map<string,vector<string> > &prePath, 65 vector<vector<string> > &res, vector<string> &tmpres, 66 string &start, string &end) 67 { 68 if(start == end) 69 { 70 reverse(tmpres.begin(), tmpres.end()); 71 res.push_back(tmpres); 72 reverse(tmpres.begin(), tmpres.end()); 73 return; 74 } 75 vector<string> &pre = prePath[end]; 76 for(int i = 0; i < pre.size(); i++) 77 { 78 tmpres.push_back(pre[i]); 79 ConstructResult(prePath, res, tmpres, start, pre[i]); 80 tmpres.pop_back(); 81 } 82 83 } 84 };
另外這一題如果不用隊列來進行bfs,可能會更加方便,使用兩個哈希表來模擬隊列,這樣還可以避免前面提到的同一個元素加入隊列多次的問題,具體可以參考這篇博客
【版權聲明】轉載請注明出處:http://www.cnblogs.com/TenosDoIt/p/3443512.html