[LeetCode 126] - 單詞梯II(Word Ladder II)


問題

給出兩個單詞(start和end)與一個字典,找出從start到end的最短轉換序列。規則如下:

  • 一次只能改變一個字母
  • 中間單詞必須在字典里存在

例如:

給出

start = "hit"
end =
"cog"
dict =
["hot","dot","dog","lot","log"]

返回

[

    ["hit","hot","dot","dog","cog"],

    ["hit","hot","lot","log","cog"]

]

注意

  • 所有單詞的長度一樣
  • 所有單詞中只有小寫字母

 

初始思路

最直接的想法是從start開始,對每個字母位置從'a'到'z'嘗試替換。如果替換字母后的單詞在字典中,將其加入路徑,然后以新單詞為起點進行遞歸調用,否則繼續循環。每層遞歸函數終止的條件是end出現或者單詞長度*26次循環完畢。end出現時表示找到一個序列,對比當前最短序列做相應更新即可。

處理過程中需要注意的主要有幾點:

  • 不能相同字母替換,如hot第一個位置遍歷到h時不應該處理。否則就會不斷的在hot上死循環。因此在替換后前要做一次檢查。
  • 我們要找的是最短的轉換方案,所以轉換序列不應該出現重復的單詞。否則組合將會有無數多種,如例子中的["hit","hot","dot","dog","dot","dog","dog",....."cog"]。這里我們可以使用一個unordered_set容器來保存某次一次替換序列中已出現過的單詞,也可以每次使用std:find去搜索當前替換序列。如果使用unordered_set,在遞歸處理時,和單詞序列一樣,要在遞歸后做相應的出棧操作。
  • 處理過程中如果發現當前處理序列長度已經超過當前最短序列長度,可以中止對該序列的處理,因為我們要找的是最短的序列。
  1 class Solution {
  2 public:
  3     std::vector<std::vector<std::string>> findLadders(std::string start, std::string end, std::unordered_set<std::string> &dict)
  4     {
  5         std::vector<std::vector<std::string>> result;
  6         std::vector<std::string> entry;
  7         
  8         entry.push_back(start);
  9         Find(start, end, dict, 0, result, entry);
 10         
 11         return result;
 12     }
 13     
 14 private:
 15     void Find(std::string& start, const std::string& end, const std::unordered_set<std::string> &dict
 16               , size_t positionToChange, std::vector<std::vector<std::string>>& result, std::vector<std::string>& entry)
 17     {
 18         //如果長度已經等於當前結果中的長度,再找出來肯定就
 19                 //超過了,終止處理
 20         if(!result.empty() && entry.size() == result[0].size())
 21         {
 22             return;
 23         }
 24         
 25         for(size_t pos = positionToChange; pos < start.size(); ++pos)
 26         {
 27             char beforeChange = ' ';
 28             for(int i = 'a'; i <= 'z'; ++i)
 29             {
 30                                 //防止同字母替換
 31                 if(start[pos] == i)
 32                 {
 33                     continue;
 34                 }
 35                 beforeChange = start[pos];
 36                 start[pos] = i;
 37                 
 38                 //用std::find的話
 39                 /*
 40                  if(std::find(entry.begin(), entry.end(), start) != entry.end())
 41                  {
 42                       start[pos] = beforeChange;
 43                       continue;
 44                  }
 45                  */
 46                                 //如果單詞已經用過的情況
 47                 if(!used_.empty() && used_.count(start)!=0 )
 48                                 {
 49                                       start[pos] = beforeChange;
 50                           continue;
 51                                 }
 52                 
 53                 
 54                 if(start == end)
 55                 {
 56                     entry.push_back(start);
 57                     
 58                                         //只需要保存最短的序列
 59                     if(!result.empty())
 60                     {
 61                         if(entry.size() < result[0].size())
 62                         {
 63                             result.clear();
 64                             result.push_back(entry);
 65                         }
 66                         else if(entry.size() == result[0].size())
 67                         {
 68                             result.push_back(entry);
 69                         }
 70                     }
 71                     else
 72                     {
 73                         result.push_back(entry);
 74                     }
 75                     //完成一個序列,把前面加入的end彈出
 76                     entry.pop_back();
 77                     return;
 78                 }
 79                 
 80                 if(dict.find(start) != dict.end())
 81                 {
 82                     entry.push_back(start);
 83                     used_.insert(start);
 84                     Find(start, end, dict, 0, result, entry);
 85                     used_.erase(*entry.rbegin());
 86                     entry.pop_back();
 87                     
 88                     
 89                     if(!entry.empty())
 90                     {
 91                         start = *entry.rbegin();
 92                     }
 93                     else
 94                     {
 95                         start[pos] = beforeChange;
 96                     }
 97                 }
 98                 else
 99                 {
100                     start[pos] = beforeChange;
101                 }
102             }
103         }
104         
105         return;
106     }
107     
108     std::unordered_set<std::string> used_;
109 };
遞歸實現

提交測試,Judge Small沒有問題。Judge Large不幸超時。

 

優化

觀察我們的處理方法,找到可變換后的單詞后,我們會馬上基於它繼續查找。這是一種深度優先的查找方法,即英文的DFS(Depth-first search)。這對找出答案很可能是不利的,如果一開始進入了一條很長的序列,就會浪費了時間。而廣度優先BFS(Breadth-first search)的方法似乎更合適,當找到一個序列時,這個序列肯定是最短的之一。

要進行廣度優先遍歷,我們可以在發現替換字母后的單詞在字典中時,不馬上繼續處理它,而是將其放入一個隊列中。通過隊列先進先出的性質,就可以實現廣度優先的處理了。由於最后要輸出的是整個轉換序列,為了簡單起見,我們可以將當前已轉換出的序列放入隊列中,即隊列形式為std::vector<std::vector<std::string>>,序列的最后一個元素就是下次處理時要繼續轉換的單詞。

使用廣度優先遍歷后,還有一個特性使得我們可以更方便的處理深度優先中重復單詞的問題。當一個單詞在某一層(一層即從第一個單詞到當前單詞的長度一樣的序列)出現后,后面再出現的情況肯定不會是最短序列(相當於走了回頭路),因此我們可以在處理完一層后直接將已用過的單詞從字典中去除。需要注意的是,同一層是不能去除的,如例子中的hot在兩個序列的第二層中都出現了。這樣我們就需要一個容器把當前層用過的單詞保存起來,等處理的層數發生變化時再將它們從字典里移除。

最后要注意的是查找結束的條件。由於找到了一個序列后該序列只是最短的之一,我們還要繼續進行處理,直到隊列中當前層都處理完畢。所以我們要在找到一個序列后將它的長度記錄下來,當要處理的序列長度已經大於該值時,就可以結束查找了。

 1 class Solution {
 2 public:
 3     std::vector<std::vector<std::string> > findLadders(std::string start, std::string end, std::unordered_set<std::string> &dict)
 4     {
 5         std::queue<std::vector<std::string>> candidate;
 6         
 7         std::vector<std::vector<std::string>> result;
 8         std::unordered_set<std::string> usedWord;
 9         
10         std::string currentString;
11         bool foundShortest = false;
12         size_t shortest = 0;
13         size_t previousPathLen = 0;
14         
15         candidate.push({start});
16         
17         while(!candidate.empty())
18         {
19             currentString = *candidate.front().rbegin();
20             
21             if(candidate.front().size() != previousPathLen)
22             {
23                 for(auto iter = usedWord.begin(); iter != usedWord.end(); ++iter)
24                 {
25                     dict.erase(*iter);
26                 }
27             }
28             
29             if(foundShortest && candidate.front().size() >= shortest)
30             {
31                 break;
32             }
33                 
34             for(size_t pos = 0; pos < start.size(); ++pos)
35             {
36                 char beforeChange = ' ';
37                 for(int i = 'a'; i <= 'z'; ++i)
38                 {
39                     beforeChange = currentString[pos];
40                     
41                     if(beforeChange == i)
42                     {
43                         continue;
44                     }
45                     
46                     currentString[pos] = i;
47                     
48                     if(dict.count(currentString) > 0)
49                     {
50                         usedWord.insert(currentString);
51                         
52                         if(currentString == end)
53                         {
54                             result.push_back(candidate.front());
55                             result.rbegin()->push_back(end);
56                             foundShortest = true;
57                             shortest = result.rbegin()->size();
58                         }
59                         else
60                         {
61                             std::vector<std::string> newPath(candidate.front());
62                             newPath.push_back(currentString);
63                             
64                             candidate.push(newPath);
65                         }
66                     }
67                     currentString[pos] = beforeChange;
68                 }
69             }
70             
71             if(!candidate.empty())
72             {
73                 previousPathLen = candidate.front().size();
74                 
75                 candidate.pop();
76             }
77         }
78         
79         return result;
80     }
81 };
BFS實現1

提交后Judge Large多處理了幾條用例,但是最后還是超時了。

 

再次優化

一個比較明顯的優化點是我們把存儲序列的vector放到了隊列中,每次都要拷貝舊隊列然后產生新隊列。回想一下我們使用vector的原因,主要是為了能保存序列,同時還能獲得當前序列的長度。為了實現這兩個目的,我們可以定義如下結構體:

 1 struct PathTag
 2 {
 3     PathTag* parent_;
 4     std::string value_;
 5     int length_;
 6        
 7     PathTag(PathTag* parent, const std::string& value, int length) : parent_(parent), value_(value), length_(length)
 8     {
 9     }
10 };

結構體記錄了當前單詞的前一個單詞以及當前的序列長度。有了這個結構體,我們在最后找到end后就可以通過不斷往前回溯得出整個路徑。而這個路徑是反向的,最后還需要做一次倒置操作。改進前面的BFS代碼如下(需要注意的是,由於LeetCode里不能使用智能指針,我們通過輔助函數來申請和釋放裸指針而不是直接new。如果不關心內存問題的話直接new了不管也可。):

  1 class Solution{
  2 public:
  3     ~Solution()
  4     {
  5         for(auto iter = pool_.begin(); iter != pool_.end(); ++iter)
  6         {
  7             delete *iter;
  8         }
  9     }
 10 
 11     std::vector<std::vector<std::string> > findLadders(std::string start, std::string end, std::unordered_set<std::string> &dict)
 12     {
 13                 std::queue<PathTag*> candidate;
 14         
 15         std::vector<std::vector<std::string>> result;
 16         std::unordered_set<std::string> usedWord;
 17         
 18         std::string currentString;
 19         bool foundShortest = false;
 20         size_t shortest = 0;
 21         size_t previousPathLen = 0;
 22         
 23         candidate.push(AllocatePathTag(nullptr, start, 1));        
 24         
 25         while(!candidate.empty())
 26         {
 27             PathTag* current = candidate.front();
 28             currentString = current->value_;
 29             
 30             if(current->length_ != previousPathLen)
 31             {
 32                 for(auto iter = usedWord.begin(); iter != usedWord.end(); ++iter)
 33                 {
 34                     dict.erase(*iter);
 35                 }
 36             }
 37             
 38             if(foundShortest && current->length_ >= shortest)
 39             {
 40                 break;
 41             }
 42             
 43             
 44             for(size_t pos = 0; pos < start.size(); ++pos)
 45             {
 46                 char beforeChange = ' ';
 47                 for(int i = 'a'; i <= 'z'; ++i)
 48                 {
 49                     beforeChange = currentString[pos];
 50                     
 51                     if(beforeChange == i)
 52                     {
 53                         continue;
 54                     }
 55                     
 56                     currentString[pos] = i;
 57                     
 58                     if(dict.count(currentString) > 0)
 59                     {
 60                         usedWord.insert(currentString);
 61                         
 62                         if(currentString == end)
 63                         {
 64                             
 65                             GeneratePath(result, current, currentString);
 66                             foundShortest = true;
 67                             shortest = result.rbegin()->size();
 68                             continue;
 69                         }
 70                         else
 71                         {
 72                             candidate.push(AllocatePathTag(current, currentString, current->length_ + 1));
 73                         }
 74                     }
 75                     currentString[pos] = beforeChange;
 76                 }
 77             }
 78             
 79             if(!candidate.empty())
 80             {
 81                 previousPathLen = current->length_;
 82                 
 83                 candidate.pop();
 84             }
 85         }
 86         
 87         return result;
 88     }
 89     
 90 private:
 91     struct PathTag
 92     {
 93         PathTag* parent_;
 94         std::string value_;
 95         int length_;
 96         
 97         PathTag(PathTag* parent, const std::string& value, int length) : parent_(parent), value_(value), length_(length)
 98         {
 99             
100         }
101         
102     };
103     
104     PathTag* AllocatePathTag(PathTag* parent, const std::string& value, int length)
105     {
106         if(nextPoolPos_ >= pool_.size())
107         {
108             for(int i = 0; i < 100; ++i)
109             {
110                 PathTag* newTag = new PathTag(nullptr, " ", 0);
111                 pool_.push_back(newTag);
112             }
113         }
114         
115         PathTag* toReturn = pool_[nextPoolPos_];
116         toReturn->parent_ = parent;
117         toReturn->value_ = value;
118         toReturn->length_ = length;
119         
120         ++nextPoolPos_;
121         
122         return toReturn;
123     }
124     
125     int nextPoolPos_;
126     
127     std::vector<PathTag*> pool_;
128     
129     void GeneratePath(std::vector<std::vector<std::string>>& result, PathTag* pathTag, const std::string& end)
130     {
131         std::vector<std::string> path;
132         
133         path.push_back(end);
134         
135         while(pathTag != nullptr)
136         {
137             path.push_back(pathTag->value_);
138             
139             pathTag = pathTag->parent_;
140         }
141         
142         size_t left = 0;
143         size_t right = path.size() - 1;
144         
145         while(left < right)
146         {
147             std::swap(path[left], path[right]);
148             
149             ++left;
150             --right;
151         }
152         
153         result.push_back(path);
154     }
155 
156 };                    
BFS2

提交后Judge Large又多處理了幾條用例,但是還是沒能通過。

 

使用鄰接列表

在繼續優化之前,我們先來學習一個圖論中的概念 - 鄰接列表(Adjacency List)。具體細節可以參見這篇wiki:http://en.wikipedia.org/wiki/Adjacency_list 。簡單來說,這是一個存儲圖中每個頂點的所有鄰接頂點的數據結構。如無向圖:

       a

   /       \

b    ---    c

它的鄰接列表為:

a => b, c

b => a, c

c => a, b

具體到本問題,我們可以發現,start到end的所有序列,就是一個這些序列中所有單詞為點組成的圖。如果我們生成了該圖的鄰接列表,就可以不斷的在每個單詞的鄰接列表里找到轉換的下一個單詞,從而最終找到end。那么,我們首先要對字典里的單詞生成鄰接列表:遍歷字典里的單詞,針對每個單詞用前面逐字母替換的方法找出鄰接單詞,並保存起來。這里使用一個std::unordered_map<std::string, std::unordered_set<std::string>>來保存鄰接列表。

有了鄰接列表,尋找序列的方法就發生變化了。我們不再逐個替換字母,而是從start出發,遍歷start的鄰接頂點,將鄰接頂點放入隊列中。並重復操作直到隊列為空。還有一個發生變化的地方是去重操作。由於不再遍歷字典,現在我們發現非同層出現重復的單詞就跳過它而不是從字典里刪去。

剩下的生成路徑的方法仍然和BFS2類似,全部代碼如下:

  1 class Solution {
  2 public:
  3     ~Solution()
  4     {
  5         for(auto iter = pool_.begin(); iter != pool_.end(); ++iter)
  6         {
  7             delete *iter;
  8         }
  9     }
 10     
 11     std::vector<std::vector<std::string> > findLadders(std::string start, std::string end, std::unordered_set<std::string> &dict)
 12     {
 13         nextPoolPos_ = 0;
 14         std::unordered_map<std::string, std::unordered_set<std::string>> adjacencyList;
 15         std::string word;
 16         
 17         for (auto iter = dict.begin(); iter != dict.end(); ++iter)
 18         {
 19             word = *iter;
 20             BuildAdjacencyList(word, adjacencyList, dict);
 21         }
 22         
 23         std::vector<std::vector<std::string>> result;
 24         std::queue<PathTag*> candidate;
 25         std::unordered_map<std::string, int> usedWord;
 26         
 27         std::string currentString;
 28         bool foundShortest = false;
 29         size_t shortest = 0;
 30         
 31         candidate.push(AllocatePathTag(nullptr, start, 1));
 32         
 33         while(!candidate.empty())
 34         {
 35             PathTag* current = candidate.front();
 36             
 37             if(foundShortest && current->length_ >= shortest)
 38             {
 39                 break;
 40             }
 41             
 42             candidate.pop();
 43             
 44             auto adjacentIter = adjacencyList.find(current->value_);
 45             
 46             if(adjacentIter != adjacencyList.end())
 47             {
 48                 for(auto iter = adjacentIter->second.begin(); iter != adjacentIter->second.end(); ++iter)
 49                 {
 50                     if(*iter == end)
 51                     {
 52                         GeneratePath(result, current, *iter);
 53                         foundShortest = true;
 54                         shortest = result.rbegin()->size();
 55                         continue;
 56                     }
 57                     
 58                     auto usedIter = usedWord.find(*iter);
 59                     
 60                     
 61                     if(usedIter != usedWord.end() && usedIter->second != current->length_ + 1)
 62                     {
 63                         continue;
 64                     }
 65                                     
 66                     usedWord[*iter] = current->length_ + 1;
 67                     
 68                    candidate.push(AllocatePathTag(current, *iter, current->length_ + 1));
 69                 }
 70             }
 71             else
 72             {
 73                 continue;
 74             }
 75             
 76         }
 77         
 78         return result;
 79     }
 80     
 81 private:
 82     struct PathTag
 83     {
 84         PathTag* parent_;
 85         std::string value_;
 86         int length_;
 87         
 88         PathTag(PathTag* parent, const std::string& value, int length) : parent_(parent), value_(value), length_(length)
 89         {        
 90         }
 91         
 92     };
 93     
 94     PathTag* AllocatePathTag(PathTag* parent, const std::string& value, int length)
 95     {
 96         if(nextPoolPos_ >= pool_.size())
 97         {
 98             for(int i = 0; i < 100; ++i)
 99             {
100                 PathTag* newTag = new PathTag(nullptr, " ", 0);
101                 pool_.push_back(newTag);
102             }
103         }
104         
105         PathTag* toReturn = pool_[nextPoolPos_];
106         toReturn->parent_ = parent;
107         toReturn->value_ = value;
108         toReturn->length_ = length;
109         
110         ++nextPoolPos_;
111         
112         return toReturn;
113     }
114     
115     int nextPoolPos_;
116     
117     std::vector<PathTag*> pool_;
118     
119     void GeneratePath(std::vector<std::vector<std::string>>& result, PathTag* pathTag, const std::string& end)
120     {
121         std::vector<std::string> path;
122         
123         path.push_back(end);
124         
125         while(pathTag != nullptr)
126         {
127                         path.push_back(pathTag->value_);
128             
129             pathTag = pathTag->parent_;
130                 }
131         
132         size_t left = 0;
133         size_t right = path.size() - 1;
134         
135         while(left < right)
136         {
137             std::swap(path[left], path[right]);
138             
139             ++left;
140             --right;
141         }
142         
143         result.push_back(path);
144     }
145     
146     void BuildAdjacencyList(std::string& word, std::unordered_map<std::string, std::unordered_set<std::string>>& adjacencyList, const std::unordered_set<std::string>& dict)
147     {
148         std::string original = word;
149         
150         for(size_t pos = 0; pos < word.size(); ++pos)
151         {
152             char beforeChange = ' ';
153             for(int i = 'a'; i <= 'z'; ++i)
154             {
155                 beforeChange = word[pos];
156                 
157                 if(beforeChange == i)
158                 {
159                     continue;
160                 }
161                 
162                 word[pos] = i;
163                 
164                 if(dict.count(word) > 0)
165                 {
166                     auto iter = adjacencyList.find(original);
167                     if(iter != adjacencyList.end())
168                     {
169                         iter->second.insert(word);
170                     }
171                     else
172                     {
173                         adjacencyList.insert(std::pair<std::string, std::unordered_set<std::string>>(original, std::unordered_set<std::string>()));
174                         adjacencyList[original].insert(word);
175                     }
176                 }
177                 
178                 word[pos] = beforeChange;
179             }
180         }
181         
182     }
183 };
鄰接列表BFS

這回終於通過Judge Large了。

 

一種更快的解決方案

下面再介紹一種更快的解決方案,思路及代碼來自niaokedaoren的博客

前一個解決方案雖然能通過大數據集測試,但是為了保存路徑信息我們額外引入了一個結構體而且因為需要用到指針使用了大量的new操作。還有什么不用保存所有路徑信息的辦法?

niaokedaoren的方案中使用了一個前驅單詞表,即記錄每一個單詞的前驅單詞是哪些。這樣在遍歷完畢后,我們從end出發遞歸就能把所有路徑生成出來。但是由於前驅單詞表不能記錄當前的層次信息,似乎我們沒法完成去重的工作。這個方案的巧妙之處就在於它沒有使用我們通常的隊列保存待處理的單詞,一個單詞一個單詞先進先出處理的方法,而是使用兩個vector來模擬隊列的操作。我們從vector 1中遍歷單詞進行轉換嘗試,發現能轉換的單詞后將其放入vector 2中。當vector 1中的單詞處理完畢后即為一層處理完畢,它里面的單詞就可以從字典里刪去了。接着我們對vector 2進行同樣處理,如此反復直到當前處理的vector中不再有單詞。我們發現vector 1和vector 2在不斷地交換正處理容器和待處理容器的身份,因此可以通過將其放入一個數組中,每次循環對數組下標值取反實現身份的交替:

int current = 0;
int previous = 1;

  循環

current = !current;
previous = !previous;
......
循環結束

完全代碼如下:

class Solution {
public:
    std::vector<std::vector<std::string> > findLadders(std::string start, std::string end, std::unordered_set<std::string> &dict)
    {
        
        result_.clear();
        std::unordered_map<std::string, std::vector<std::string>> prevMap;
        
        for(auto iter = dict.begin(); iter != dict.end(); ++iter)
        {
            prevMap[*iter] = std::vector<std::string>();
        }
        
        std::vector<std::unordered_set<std::string>> candidates(2);
        
        int current = 0;
        int previous = 1;
        
        candidates[current].insert(start);
        
        while(true)
        {
            current = !current;
            previous = !previous;
            
            for (auto iter = candidates[previous].begin(); iter != candidates[previous].end(); ++iter)
            {
                dict.erase(*iter);
            }
            
            candidates[current].clear();
            
            for(auto iter = candidates[previous].begin(); iter != candidates[previous].end(); ++iter)
            {
                for(size_t pos = 0; pos < iter->size(); ++pos)
                {
                    std::string word = *iter;
                    for(int i = 'a'; i <= 'z'; ++i)
                    {
                        if(word[pos] == i)
                        {
                            continue;
                        }
                        
                        word[pos] = i;
                        
                        if(dict.count(word) > 0)
                        {
                            prevMap[word].push_back(*iter);
                            candidates[current].insert(word);
                        }
                    }
                }
            }
            
            if (candidates[current].size() == 0)
            {
                return result_;
            }
            if (candidates[current].count(end))
            {
                break;
            }
        }
        
        
        std::vector<std::string> path;
        GeneratePath(prevMap, path, end);
        
        return result_;
    }
    
private:
    void GeneratePath(std::unordered_map<std::string, std::vector<std::string>> &prevMap, std::vector<std::string>& path,
                      const std::string& word)
    {
        if (prevMap[word].size() == 0)
        {
            path.push_back(word);
            std::vector<std::string> curPath = path;
            reverse(curPath.begin(), curPath.end());
            result_.push_back(curPath);
            path.pop_back();
            return;
        }
        
        path.push_back(word);
        for (auto iter = prevMap[word].begin(); iter != prevMap[word].end(); ++iter)
        {
            GeneratePath(prevMap, path, *iter);
        }
        path.pop_back();
    }
    
    std::vector<std::vector<std::string>> result_;
};
niaokedaoren的方案

可以看到處理速度快了不少:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM