题目很少说了。见https://oj.leetcode.com/problems/word-ladder-ii/web
这一题我反复修改了两天半。尝试过各类思路,老是报TLE。终于知道这一题为何是leetcode上经过率最低的一道题了,它对时限的要求实在太苛刻了。算法
在我AC版本代码的前一个版本,最好也就过了单词长度为7的test case。而后就TLE了。数据结构
到底问题在哪儿?我从算法,STL数据结构,代码优化各类角度思考。比较惋惜的是,直到最后我也没有弄清为啥能AC,为啥会TLE。(都是我写的代码,都是个人思路,太诡异了。。。)ide
但无论如何,经过这一题,学到的还真是挺多。这里总结下吧。模块化
拿到这一题的时候,首先想到的就是爆搜。依次替换单词中的字母,而后依次为基础进行搜索。优化
是BFS仍是DFS呢?spa
先引用下Stack Overflow上的两个解答rest
That heavily depends on the structure of the search tree and the number and location of solutions. If you know a solution is not far from the root of the tree, a breadth first search (BFS) might be better. If the tree is very deep and solutions are rare, depth first search (DFS) might take an extremely long time, but BFS could be faster. If the tree is very wide, a BFS might need too much memory, so it might be completely impractical. If solutions are frequent but located deep in the tree, BFS could be impractical. If the search tree is very deep you will need to restrict the search depth for depth first search (DFS), anyway (for example with iterative deepening).code
--------------------------------------------------------------------------------------orm
BFS is going to use more memory depending on the branching factor... however, BFS is a complete algorithm... meaning if you are using it to search for something in the lowest depth possible, BFS will give you the optimal solution. BFS space complexity is O(b^d)
... the branching factor raised to the depth (can be A LOT of memory).
DFS on the other hand, is much better about space however it may find a suboptimal solution. Meaning, if you are just searching for a path from one vertex to another, you may find the suboptimal solution (and stop there) before you find the real shortest path. DFS space complexity is O(|V|)
... meaning that the most memory it can take up is the longest possible path.
They have the same time complexity.
其实这一题很容易在脑海汇中勾勒一下DFS/BFS搜索树的大体样子。
若是选用DFS(即广义上的爆搜递归)
void search(string &word, string &end, unordered_set<string> &dict, int level) { if(word == end) return; if( level == dict.size()) return; for(int i = 0; i < word.length(); i++) { for(int ch = 'a'; j <='z'; j++) { string tmp = word; if(tmp[i] == ch) continue; tmp[i] = ch; if(dict.count(tmp) > 0) search(tmp, end, dict, level+1); } }
如此,必需要遍历整棵搜索树,记录全部可能的解路径,而后比较最短的输出,重复节点不少,时间复杂度至关大。有人问能够剪枝么,答案是这里无法剪。若是把已经访问过的剪掉,那么就会出现搜索不彻底的状况。
看来直接上来爆搜是不行的。效率低的不能忍。
这样看,若是将相邻的两个单词(即只差一个字母的单词)相互连在一块儿,这就是一个图嘛。经典的图算法,dijiska算法不就是求解最短路径的算法么。
那么就说直接邻接表建图,而后dijkstra算法求解咯,固然是能够的,边缘权值设为1就行。并且这种思路工程化,模块化思路很明显,比较不容易出错。但此种状况下时间需建图,而后再调用dijkstra,光是后者复杂度就为o(n^2),因此仍有可能超时,或者说,至少还不是最优方法。
建图后进行DFS呢。很惋惜,对于一个无向有环图,DFS只能遍历节点,求最短路径什么的仍是别想了。(注意,这里对图进行DFS搜索也会生成一颗搜索树,可是与上文提到的递归爆搜获得的搜索树彻底不同哦,主要是由于对图进行DFS得不到严谨的先后关系,而这是最短路径必须具有的)
好了,咱们来看看一个例子
如何对这个图进行数据结构上的优化,算法上的优化是解决问题的关键。
经过观察,容易发现这个图没有边权值,也就是所用dijkstra算法显得不必了,简单的BFS就行,呵呵,BFS是能够求这类图的最短路径的,
正如wiki所言:若全部边的长度相等,广度优先搜索算法是最佳解——亦即它找到的第一个解,距离根节点的边数目必定最少。
因此,从出发点开始,第一次"遍历"到终点时过的那条路径就是最短的路径。并且是时间复杂度为O(|V|+|E|)。时间复杂度较dijkstra小,尤为是在边没那么多的时候。
到此为止了么。固然不是,还能够优化。
回到最原始的问题,这个图够好么?它能反映问题的本质么。所谓问题的本质,有这么两点,一是具备严格的先后关系(由于要输出全部变换序列),二是图中的边数量是否过大,可以减少一些呢?
其实,一个相对完美的图应该是这样的
这个图有两个很明显的特色,一是有向图,具备鲜明的层次特性,二是边没有冗余。此图完美的描述了解的结构。
因此,咱们建图也要有必定策略,也许大家会问,我是怎么想出来的。
其实,能够这样想,咱们对一个单词w进行单个字母的变换,获得w1 w2 w3...,本轮的这些替换结果直接做为当前单词w的后继节点,借助BFS的思想,将这些节点保存起来,下一轮开始的时候提取将这些后继节点做为新的父节点,而后重复这样的步骤。
这里,咱们须要对节点“分层”。上图很明显分为了三层。这里没有用到队列,可是思想和队列一致的。由于队列没法体现层次关系,因此建图的时候,必须设立两个数据结构,用来保存当前层和下层,交替使用这两个数据结构保存父节点和后继节点。
同时,还须要保证,当前层的全部节点必须不一样于全部高层的节点。试想,若是tot下面又接了一个pot,那么由此构造的路径只会比tot的同层pot构造出的路径长。如何完成这样的任务呢?能够这样,咱们把全部高层节点从字典集合中删除,而后供给当前层选取单词。这样,当前层选取的单词就不会与上层的重复了。注意,每次更新字典的时候是在当前层处理完毕以后在更新,切不可获得一个单词就更新字典。例如咱们获得了dog,不能立刻把dog从待字典集合中删除,不然,下次hog生成dog时在字典中找不到dog,从而致使结果不完整。简单的说,同层的节点能够重复。上图也能够把dog化成两个节点,由dot和hog分别指向。我这里为了简单就没这么画了。
最后生成的数据结构应该这样,相似邻接表
hot---> hop, tot, dot, pot, hog
dot--->dog
hog--->dog, cog
ok。至此,问题算是基本解决了,剩下的就是如何生成路径。其实很简单,对于这种“特殊”的图,咱们能够直接DFS搜索,节点碰到目标单词就返回。
这就完了,不能优化了?不,还能够优化。
能够看到,在生成路径的时候,若是可以从下至上搜索的话,就能够避免那些无用的节点,好比hop pot tot这类的,大大提高效率。其实也简单,构造数据结构时,交换一下节点,以下图
dog--->dot, hog
cog--->hog
hop--->hot
tot--->hot
dot--->hot
pot--->hot
hog--->hot
说白了,构造一个反向邻接表便可。
对了,还没说整个程序的终止条件。若是找到了,把当前层搜完就退出。若是没找到,字典早晚会被清空,这时候退出就行。
说了这么多,上代码吧
1 class Solution { 2 public: 3 vector<string> temp_path; 4 vector<vector<string>> result_path; 5 6 void GeneratePath(unordered_map<string, unordered_set<string>> &path, const string &start, const string &end) 7 { 8 temp_path.push_back(start); 9 if(start == end) 10 { 11 vector<string> ret = temp_path; 12 reverse(ret.begin(),ret.end()); 13 result_path.push_back(ret); 14 return; 15 } 16 17 for(auto it = path[start].begin(); it != path[start].end(); ++it) 18 { 19 GeneratePath(path, *it, end); 20 temp_path.pop_back(); 21 } 22 } 23 vector<vector<string>> findLadders(string start, string end, unordered_set<string> &dict) 24 { 25 temp_path.clear(); 26 result_path.clear(); 27 28 unordered_set<string> current_step; 29 unordered_set<string> next_step; 30 31 unordered_map<string, unordered_set<string>> path; 32 33 unordered_set<string> unvisited = dict; 34 35 if(unvisited.count(start) > 0) 36 unvisited.erase(start); 37 38 current_step.insert(start); 39 40 while( current_step.count(end) == 0 && unvisited.size() > 0 ) 41 { 42 for(auto pcur = current_step.begin(); pcur != current_step.end(); ++pcur) 43 { 44 string word = *pcur; 45 46 for(int i = 0; i < start.length(); ++i) 47 { 48 for(int j = 0; j < 26; j++) 49 { 50 string tmp = word; 51 if( tmp[i] == 'a' + j ) 52 continue; 53 tmp[i] = 'a' + j; 54 if( unvisited.count(tmp) > 0 ) 55 { 56 next_step.insert(tmp); 57 path[tmp].insert(word); 58 } 59 } 60 } 61 } 62 63 if(next_step.empty()) break; 64 for(auto it = next_step.begin() ; it != next_step.end(); ++it) 65 { 66 unvisited.erase(*it); 67 } 68 69 current_step = next_step; 70 next_step.clear(); 71 } 72 73 if(current_step.count(end) > 0) 74 GeneratePath(path, end, start); 75 76 return result_path; 77 } 78 };
此外,这里还有一份代码,写的比较乱,但用的传统队列的思想,用两个标记变量来指示层数的变化。也AC了。
class Solution { public: vector<vector<string>> output; vector<string> cur; void FindPath(unordered_map<string, unordered_set<string>> &graph, const string &start, const string &end) { cur.push_back(start); if(start == end) { vector<string> ret = cur; reverse(ret.begin(),ret.end()); output.push_back(ret); return; } for(auto it2 = graph[start].begin(); it2 != graph[start].end(); ++it2) { FindPath(graph, *it2, end); cur.pop_back(); } } vector<vector<string>> findLadders(string start, string end, unordered_set<string> & _dict) { unordered_set<string> dict = _dict; if(dict.count(start) >0) dict.erase(start); output.clear(); cur.clear(); unordered_map<string, unordered_set<string>> graph; queue<string> q; unordered_map<string, int> depth; q.push(start); depth[start] = 0; bool found = false; int cur_deep = 0; int pre_deep = 0; while(!q.empty()) { string word = q.front(); q.pop(); pre_deep = cur_deep; cur_deep = depth[word]; if(pre_deep != cur_deep) { if(depth.count(end) > 0) { found = true; break; } else if(depth.size() == dict.size() + 1) break; } for( int i = 0; i < start.length(); ++i) { for(char ch = 'a'; ch <= 'z'; ch++) { string tmp = word; if(tmp[i] != ch) { tmp[i] = ch; int t = depth.count(tmp); if((t == 0 && dict.count(tmp) > 0) || (t > 0 && depth[tmp] == cur_deep + 1) ) { graph[tmp].insert(word);
if(t == 0)
{ q.push(tmp);
depth[tmp] = cur_deep + 1;
} } } } } } if(found) { FindPath(graph, end, start); } return output; } };