题目不多说了。见https://oj.leetcode.com/problems/word-ladder-ii/

这一题我反复修改了两天半。尝试过各种思路,总是报TLE。终于知道这一题为什么是leetcode上通过率最低的一道题了,它对时限的要求实在太苛刻了。

在我AC版本代码的前一个版本,最好也就过了单词长度为7的test case。然后就TLE了。

到底问题在哪儿?我从算法,STL数据结构,代码优化各种角度思考。比较可惜的是,直到最后我也没有弄清为啥能AC,为啥会TLE。(都是我写的代码,都是我的思路,太诡异了。。。)

但不管如何,通过这一题,学到的还真是挺多。这里总结下吧。

拿到这一题的时候,首先想到的就是爆搜。依次替换单词中的字母,然后依次为基础进行搜索。

是BFS还是DFS呢?

先引用下Stack Overflow上的两个解答

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).

--------------------------------------------------------------------------------------

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 = ; 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) > )
search(tmp, end, dict, level+);
}
}

如此,必须要遍历整棵搜索树,记录所有可能的解路径,然后比较最短的输出,重复节点很多,时间复杂度相当大。有人问可以剪枝么,答案是这里没法剪。如果把已经访问过的剪掉,那么就会出现搜索不完全的情况。

看来直接上来爆搜是不行的。效率低的不能忍。

这样看,如果将相邻的两个单词(即只差一个字母的单词)相互连在一起,这就是一个图嘛。经典的图算法,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

说白了,构造一个反向邻接表即可。

对了,还没说整个程序的终止条件。如果找到了,把当前层搜完就退出。如果没找到,字典迟早会被清空,这时候退出就行。

说了这么多,上代码吧

 class Solution {
public:
vector<string> temp_path;
vector<vector<string>> result_path; void GeneratePath(unordered_map<string, unordered_set<string>> &path, const string &start, const string &end)
{
temp_path.push_back(start);
if(start == end)
{
vector<string> ret = temp_path;
reverse(ret.begin(),ret.end());
result_path.push_back(ret);
return;
} for(auto it = path[start].begin(); it != path[start].end(); ++it)
{
GeneratePath(path, *it, end);
temp_path.pop_back();
}
}
vector<vector<string>> findLadders(string start, string end, unordered_set<string> &dict)
{
temp_path.clear();
result_path.clear(); unordered_set<string> current_step;
unordered_set<string> next_step; unordered_map<string, unordered_set<string>> path; unordered_set<string> unvisited = dict; if(unvisited.count(start) > )
unvisited.erase(start); current_step.insert(start); while( current_step.count(end) == && unvisited.size() > )
{
for(auto pcur = current_step.begin(); pcur != current_step.end(); ++pcur)
{
string word = *pcur; for(int i = ; i < start.length(); ++i)
{
for(int j = ; j < ; j++)
{
string tmp = word;
if( tmp[i] == 'a' + j )
continue;
tmp[i] = 'a' + j;
if( unvisited.count(tmp) > )
{
next_step.insert(tmp);
path[tmp].insert(word);
}
}
}
} if(next_step.empty()) break;
for(auto it = next_step.begin() ; it != next_step.end(); ++it)
{
unvisited.erase(*it);
} current_step = next_step;
next_step.clear();
} if(current_step.count(end) > )
GeneratePath(path, end, start); return result_path;
}
};

此外,这里还有一份代码,写的比较乱,但用的传统队列的思想,用两个标记变量来指示层数的变化。也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) >)
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] = ; bool found = false; int cur_deep = ;
int pre_deep = ; 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) > )
{
found = true;
break;
}
else if(depth.size() == dict.size() + )
break;
} for( int i = ; 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 == && dict.count(tmp) > ) || (t > && depth[tmp] == cur_deep + ) )
{
graph[tmp].insert(word);
if(t == )
{
q.push(tmp);
depth[tmp] = cur_deep + ;
}
}
}
}
}
} if(found)
{
FindPath(graph, end, start);
} return output;
}
};

leetcode 解题报告 Word Ladder II的更多相关文章

  1. 【LeetCode OJ】Word Ladder II

    Problem Link: http://oj.leetcode.com/problems/word-ladder-ii/ Basically, this problem is same to Wor ...

  2. LeetCode解题报告—— Word Search & Subsets II & Decode Ways

    1. Word Search Given a 2D board and a word, find if the word exists in the grid. The word can be con ...

  3. 【leetcode】126. Word Ladder II

    题目如下: 解题思路:DFS或者BFS都行.本题的关键在于减少重复计算.我采用了两种方法:一是用字典dic_ladderlist记录每一个单词可以ladder的单词列表:另外是用dp数组记录从star ...

  4. LeetCode解题报告—— Permutations & Permutations II & Rotate Image

    1. Permutations Given a collection of distinct numbers, return all possible permutations. For exampl ...

  5. leetcode 解题报告 Word Break

    Given a string s and a dictionary of words dict, determine if s can be segmented into a space-separa ...

  6. LeetCode: Word Ladder II 解题报告

    Word Ladder II Given two words (start and end), and a dictionary, find all shortest transformation s ...

  7. [leetcode]Word Ladder II @ Python

    [leetcode]Word Ladder II @ Python 原题地址:http://oj.leetcode.com/problems/word-ladder-ii/ 参考文献:http://b ...

  8. [Leetcode Week5]Word Ladder II

    Word Ladder II 题解 原创文章,拒绝转载 题目来源:https://leetcode.com/problems/word-ladder-ii/description/ Descripti ...

  9. LeetCode解题报告:Linked List Cycle && Linked List Cycle II

    LeetCode解题报告:Linked List Cycle && Linked List Cycle II 1题目 Linked List Cycle Given a linked ...

随机推荐

  1. MYSQL 的异常CRASH事件处理

    检查问题的过程****************************************************************************************** ps ...

  2. shell字符串变量的特异功能:字符串的替换(${str/源模式/目标模式},${str//源模式/目标模式})、截断

    https://blog.csdn.net/wzb56_earl/article/details/6953612

  3. C# 对话框使用大全

    对话框中我们常用了以下几种:1.文件对话框(FileDialog) 它又常用到两个: 打开文件对话框(OpenFileDialog) 保存文件对话(SaveFileDialog)2.字体对话框(Fon ...

  4. Jquery EasyUI选项卡-Tabs的使用方法

    以下是easyUI的tabs的简单实用介绍. var e =$('#main').tabs('exists','accordion'); if(e==true){  $('#main').tabs(' ...

  5. B - ACM小组的古怪象棋 【地图型BFS+特殊方向】

    ACM小组的Samsara和Staginner对中国象棋特别感兴趣,尤其对马(可能是因为这个棋子的走法比较多吧)的使用进行深入研究.今天他们又在 构思一个古怪的棋局:假如Samsara只有一个马了,而 ...

  6. HDU 多校1.5

    Expectation Division Time Limit: 6000/3000 MS (Java/Others)    Memory Limit: 131072/131072 K (Java/O ...

  7. Cocos2d-Lua 做一个活动转盘

    这类活动你肯定见过 关于转盘类型的活动我相信大家多多少少都接触到了,很多的抽奖界面都是这类型的,今天这篇小文章就简单的总结一下我们游戏中需要实现这样一个效果的时候我们该怎样去做,其实只要是Cocos类 ...

  8. 1087: Common Substrings (哈希)

    1087: Common Substrings Time Limit:3000/1000 MS (Java/Others)   Memory Limit:163840/131072 KB (Java/ ...

  9. sqlplus terminators - Semicolumn (;), slash (/) and a blank line

    The problem here is the way SQL*Plus interprets the commands passed to it. Remember the "SQL co ...

  10. luogu P3368 【模板】树状数组 2

    题目描述 如题,已知一个数列,你需要进行下面两种操作: 1.将某区间每一个数数加上x 2.求出某一个数的和 输入输出格式 输入格式: 第一行包含两个整数N.M,分别表示该数列数字的个数和操作的总个数. ...