Leetcode | Word Ladder
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.
拿到题目后的第一想法就是BFS,然后要注意怎么找到它的邻居。假设dict里有m个word,每个word的长度为n,找邻居有三种方式:
1. 遍历dict里所有的word,判断与当前word相交是不是只是一个字符,那么每次找邻居的时间复杂度为O(nm)。
2. 预先计算好所有的word之间是不是可以相互转换,O(nm^2),显然不适合;
3. 遍历当前word的每个位置,把当前位置变成另一个字符,判断新产生的word是不是在dict里,时间复杂度为(26*n);
综上,选择第3种方式。
第二个注意点就是BFS的退出条件,如果放在queue的pop之后再判断是否等于end,那么就会额外地多一些开销,最好就是在找邻居的时候,如果遇到了end就直接退出。此时return的就是当前的层数。
BFS的层数计算的话,我习惯于用一个空串作为哨兵,但是这样循环的判断条件就要变成q.size() > 1.
1320ms,好慢,还是通过了。
1 class Solution { 2 public: 3 int ladderLength(string start, string end, unordered_set<string> &dict) { 4 if (dict.empty()) return 0; 5 if (start.empty() || end.empty()) return 0; 6 if (start.length() != end.length()) return 0; 7 dict.insert(end); 8 9 queue<string> q; 10 q.push(start); 11 q.push(""); 12 int n = start.length(); 13 14 int h = 1; 15 while (q.size() > 1) { 16 string tmp = q.front(); 17 q.pop(); 18 19 if (tmp.empty()) { h++; q.push(""); continue;} 20 21 for (int i = 0; i < n; ++i) { 22 for (char c = 'a'; c <= 'z'; ++c) { 23 string next(tmp); 24 next[i] = c; 25 if (next == end) return h + 1; 26 if (next != tmp && dict.find(next) != dict.end()) { 27 q.push(next); 28 dict.erase(next); 29 } 30 } 31 } 32 } 33 34 return 0; 35 } 36 };
优化
1. 将第23行(string next(tmp))提到外循环,可以从1320ms降到900ms。
2. 将层数的判断用一个endOfLayer来判断,循环条件改回!q.empty(),可以再降到856ms。
3. 不要判断next != tmp(Line 26),而是用tmp[i] == c 里 直接continue; 再降到740ms。
4. 将return h + 1;放在dict.find找到之后再判断; 再降到620ms。
5. 将dict.find改成dict.count(next) > 0。降到460ms。
这首题是第一次尝试一步一步地优化,在内循环里面的每一个判断,每一步计算都要仔细考虑,这样才能达到更优。
1 class Solution { 2 public: 3 int ladderLength(string start, string end, unordered_set<string> &dict) { 4 if (dict.empty()) return 0; 5 if (start.empty() || end.empty()) return 0; 6 if (start.length() != end.length()) return 0; 7 dict.insert(end); 8 9 queue<string> q; 10 q.push(start); 11 string endOfLayer = start; 12 int n = start.length(); 13 14 int h = 1; 15 while (!q.empty()) { 16 string tmp = q.front(); 17 q.pop(); 18 19 for (int i = 0; i < n; ++i) { 20 string next(tmp); 21 for (char c = 'a'; c <= 'z'; ++c) { 22 if (tmp[i] == c) continue; 23 next[i] = c; 24 25 if (dict.count(next) > 0) { 26 if (next == end) return h + 1; 27 q.push(next); 28 dict.erase(next); 29 } 30 } 31 } 32 33 if (endOfLayer == tmp) { 34 endOfLayer = q.back(); 35 h++; 36 } 37 } 38 39 return 0; 40 } 41 };
Method II
从网上看到有人用两个vector来模拟queue的层数变换。
480ms
1 class Solution { 2 public: 3 int ladderLength(string start, string end, unordered_set<string> &dict) { 4 if (dict.empty()) return 0; 5 if (start.empty() || end.empty()) return 0; 6 if (start.length() != end.length()) return 0; 7 dict.insert(end); 8 9 vector<unordered_set<string> > layers(2); 10 int cur = 0, pre = 1; 11 layers[pre].insert(start); 12 dict.erase(start); 13 14 int n = start.length(); 15 16 int h = 1; 17 while (!layers[pre].empty()) { 18 layers[cur].clear(); 19 for (unordered_set<string>::iterator it = layers[pre].begin(); it != layers[pre].end(); ++it) { 20 for (int i = 0; i < n; ++i) { 21 string next(*it); 22 for (char c = 'a'; c <= 'z'; ++c) { 23 if ((*it)[i] == c) continue; 24 next[i] = c; 25 26 if (dict.count(next) > 0) { 27 if (next == end) return h + 1; 28 layers[cur].insert(next); 29 dict.erase(next); 30 } 31 } 32 } 33 } 34 35 cur = !cur; 36 pre = !pre; 37 h++; 38 } 39 40 return 0; 41 } 42 };
Bug
另外,我尝试过queue里面保存的是unorder_set<string>::iterator,在本地主机上测试可以,在OJ上就runtime error了。第二天仔细一想,原来是真有bug。
见以下代码:
1 int ladderLength(string start, string end, unordered_set<string> &dict) { 2 if (dict.empty()) return 0; 3 if (start.empty() || end.empty()) return 0; 4 if (start.length() != end.length()) return 0; 5 dict.insert(start); 6 dict.insert(end); 7 8 queue<unordered_set<string>::iterator> q; 9 q.push(dict.find(start)); 10 q.push(dict.end()); 11 int n = start.length(); 12 13 int h = 1; 14 while (q.size() > 1) { 15 unordered_set<string>::iterator t = q.front(); 16 q.pop(); 17 18 if (t == dict.end()) { h++; q.push(dict.end()); continue;} 19 string tmp(*t); 20 dict.erase(t); 21 22 for (int i = 0; i < n; ++i) { 23 for (char c = 'a'; c <= 'z'; ++c) { 24 string next(tmp); 25 next[i] = c; 26 if (end == tmp) return h; 27 if (next == tmp) continue; 28 unordered_set<string>::iterator it = dict.find(next); 29 if (it != dict.end()) { 30 q.push(it); 31 } 32 } 33 } 34 } 35 36 return 0; 37 }
首先用指针的话,那么就只能在pop queue的时候erase了(Line 20)。如果有两个word 'a' 和'b',他们都能一步到达'c',那么第一次的时候,那么'c'就会被插入队列两次。因此,'c'对应的这个指针也就会被erase两次!!!当然就是runtime error了。
不晓得为什么本地主机这么个bug都没有重现出来。。。。。。