zoukankan      html  css  js  c++  java
  • Word Ladder II——找出两词之间最短路径的所有可能

    Given two words (start and end), and a dictionary, find all shortest transformation sequence(s) from start to end, such that:

    1. Only one letter can be changed at a time
    2. 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.

    我的写法最后的结果顺序与答案不符

     1 class Solution {
     2 public:
     3     vector<vector<string>> findLadders(string start, string end, unordered_set<string> &dict) {
     4         bfs(start,end,dict);
     5         v.push_back(end);
     6         dfs(end,start,dict);
     7         return res;
     8     }
     9     
    10     void bfs(string start, string end, unordered_set<string> &dict){
    11         queue<string> q;
    12         q.push(start);
    13         m[start]=1;
    14         min=dict.size()+2;
    15         while(!q.empty()){
    16             string cur=q.front();
    17             q.pop();
    18             for(int i=0;i<cur.length();i++){
    19                 for(char c='a';c<='z';c++){
    20                     if(c==cur[i])
    21                         continue;
    22                     string tmp=cur;
    23                     tmp[i]=c;
    24                     if(tmp==end){
    25                         min=m[cur]+1<min?m[cur]+1:min;
    26                         m[end]=min;
    27                         continue;
    28                     }
    29                     if(dict.find(tmp)!=dict.end()&&m.find(tmp)==m.end()){
    30                         q.push(tmp);
    31                         m[tmp]=m[cur]+1;
    32                     }
    33                 }
    34             }
    35         }
    36     }
    37     void dfs(string start, string end, unordered_set<string> &dict){
    38         if(start==end){
    39             vector<string> tmp;
    40             for(int i=v.size()-1;i>=0;i--){
    41                 tmp.push_back(v[i]);
    42             }
    43             res.push_back(tmp);
    44             return;
    45         }
    46         for(int i=0;i<start.length();i++){
    47             for(char c='a';c<='z';c++){
    48                 if(c==start[i])
    49                     continue;
    50                 string cur=start;
    51                 cur[i]=c;
    52                 if(m.find(cur)!=m.end()&&m[cur]==(m[start]-1)){
    53                     v.push_back(cur);
    54                     dfs(cur,end,dict);
    55                     v.pop_back();
    56                 }
    57             }
    58         }
    59     }
    60     vector<vector<string>> res;
    61     vector<string> v;
    62     map<string,int> m;
    63     int min=0;
    64 };

    分析转自:http://www.cnblogs.com/ShaneZhang/p/3748494.html

    其实这一题很容易在脑海汇中勾勒一下DFS/BFS搜索树的大致样子。

    如果选用DFS(即广义上的爆搜递归)

     1 void search(string &word, string &end, unordered_set<string> &dict, int level)
     2 {
     3      if(word == end)
     4          return;
     5      
     6      if( level == dict.size())
     7          return;
     8      
     9      for(int i = 0; i < word.length(); i++)
    10     {
    11            for(int ch = 'a'; j <='z'; j++)
    12            {
    13                     string tmp = word;
    14                     if(tmp[i] == ch)
    15                              continue;
    16                     tmp[i] = ch;
    17                     if(dict.count(tmp) > 0)
    18                           search(tmp, end, dict, level+1);                  
    19             }        
    20     }   

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

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

    这样看,如果将相邻的两个单词(即只差一个字母的单词)相互连在一起,这就是一个图嘛。经典的图算法,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 };
  • 相关阅读:
    SQL SERVER中一些常见性能问题的总结
    【BZOJ2554】Color 概率神题
    【BZOJ1818】[Cqoi2010]内部白点 扫描线+树状数组
    【BZOJ1879】[Sdoi2009]Bill的挑战 状压DP
    【BZOJ2668】[cqoi2012]交换棋子 费用流
    【BZOJ4372】烁烁的游戏 动态树分治+线段树
    【BZOJ3470】Freda’s Walk 概率与期望
    【BZOJ2087】[Poi2010]Sheep 几何+DP
    【BZOJ4428】[Nwerc2015]Debugging调试 记忆化搜索+分块
    【BZOJ2137】submultiple 高斯消元求伯努利数
  • 原文地址:https://www.cnblogs.com/zl1991/p/7000472.html
Copyright © 2011-2022 走看看