LeetCode: Word Ladder I && II
Given two words (beginWord and endWord), and a dictionary, find the length of shortest transformation sequence from beginWord to endWord, 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.
思路:开始考虑了下使用递归和从两边夹逼,但是做不先去。实际上就是一个BFS。对于求最小路径,BFS最适合不过了啊!!!
虽然采用BFS,但是仍需要注意一些问题。就是访问之后需不需要删除的问题。
1. 将每个单词看成图的一个节点。
2. 当单词s1改变一个字符可以变成存在于字典的单词s2时,则s1与s2之间有连接。
3. 给定s1和s2,问题I转化成了求在图中从s1->s2的最短路径长度。而问题II转化为了求所有s1->s2的最短路径。
无论是求最短路径长度还是求所有最短路径,都是用BFS。在BFS中有三个关键步骤需要实现:
1. 如何找到与当前节点相邻的所有节点。
这里可以有两个策略:
(1) 遍历整个字典,将其中每个单词与当前单词比较,判断是否只差一个字符。复杂度为:n*w,n为字典中的单词数量,w为单词长度。
(2) 遍历当前单词的每个字符x,将其改变成a~z中除x外的任意一个,形成一个新的单词,在字典中判断是否存在。复杂度为:26*w,w为单词长度。
这里可以和面试官讨论两种策略的取舍。对于通常的英语单词来说,长度大多小于100,而字典中的单词数则往往是成千上万,所以策略2相对较优。
2. 如何标记一个节点已经被访问过,以避免重复访问。
可以将访问过的单词从字典中删除。
class Solution { public: int ladderLength(string start, string end, unordered_set<string> &dict) { dict.insert(end); queue<pair<string,int>> q; q.push(make_pair(start,1)); dict.erase(start); while(!q.empty()) { string s = q.front().first; int len = q.front().second; if(s==end) return len; q.pop(); vector<string> neighbors = findNeighbors(s, dict); for(int i=0; i<neighbors.size(); i++) q.push(make_pair(neighbors[i],len+1)); } return 0; } vector<string> findNeighbors(string s, unordered_set<string> &dict) { vector<string> ret; for(int i=0; i<s.size(); i++) { char c = s[i]; for(int j=0; j<26; j++) { if(c=='a'+j) continue; s[i] = 'a'+j; if(dict.count(s)) { ret.push_back(s); dict.erase(s); } } s[i] = c; } return ret; } };
II
title:
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)
class Solution { public: vector<vector<string>> findLadders(string start, string end, unordered_set<string> &dict) { vector<vector<string> > results; unordered_map<string, vector<string> > traces; dict.insert(end); queue<pair<string,int>> q; q.push(make_pair(start,1)); set<string> level_set; level_set.insert(start); int level = 0; int min_level = INT_MAX; while(!q.empty()) { string s = q.front().first; int len = q.front().second; if (len > min_level) break; if (len != level){ for (set<string>::iterator iter = level_set.begin(); iter != level_set.end(); iter++){ dict.erase(*iter); } level = len; level_set.clear(); } if(s==end){ min_level = level; vector<string> result; result.push_back(s); buildPathFromTraces(traces,end,start,result,results); } q.pop(); vector<string> neighbors = findNeighbors(s, dict,level_set); for(int i=0; i<neighbors.size(); i++){ if (level_set.find(neighbors[i]) == level_set.end()){ q.push(make_pair(neighbors[i],len+1)); level_set.insert(neighbors[i]); } traces[neighbors[i]].push_back(s); } } return results; } void buildPathFromTraces(unordered_map<string,vector<string> > &traces, string end, string start, vector<string> &result, vector<vector<string> > &results){ if (end == start){ reverse(result.begin(), result.end()); results.push_back(result); reverse(result.begin(), result.end()); return ; } vector<string> pre = traces[end]; for (int i = 0 ; i < pre.size(); i++){ result.push_back(pre[i]); buildPathFromTraces(traces,pre[i],start,result,results); result.pop_back(); } } vector<string> findNeighbors(string s, unordered_set<string> &dict, set<string> &level_set) { vector<string> ret; for(int i=0; i<s.size(); i++) { char c = s[i]; for(int j=0; j<26; j++) { if(c=='a'+j) continue; s[i] = 'a'+j; if(dict.count(s)) { ret.push_back(s); } } s[i] = c; } return ret; } };