ladder word——图的广度优先遍历

**

Word Ladder Ⅰ

**
Problem
Given two words (start and end), and a dictionary, find the length ofshortest 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
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.

题中有两大关键点:一次只能改动一个字符;改动的中间结果必须出现在词典中。由于中间结果也必须出现在词典中,故此题相当于图搜索问题,将 start, end, dict 中的单词看做图中的节点,节点与节点(单词与单词)可通过一步转化得到,可以转换得到的节点相当于边的两个节点,边的权重为1(都是通过1步转化)。到这里问题就比较明确了,相当于搜索从 start 到 end 两点间的最短距离,即 Dijkstra 最短路径算法。通过 BFS 和哈希表实现。

首先将 start 入队,随后弹出该节点,比较其和 end 是否相同;再从 dict 中选出所有距离为1的单词入队,并将所有与当前节点距离为1且未访问过的节点(需要使用哈希表)入队,方便下一层遍历时使用,直至队列为空。

int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
    unordered_set<string> dict(wordList.begin(), wordList.end());
    unordered_map<string, int> m;
    queue<string> q;
    m[beginWord] = 1;
    q.push(beginWord);
    while (!q.empty()) {
        string word = q.front(); q.pop();
        for (int i = 0; i < word.size(); ++i) {
            string newWord = word;
            for (char ch = 'a'; ch <= 'z'; ++ch) {
                newWord[i] = ch;
                if (dict.count(newWord) && newWord == endWord) 
                    return m[word] + 1;
                if (dict.count(newWord) && !m.count(newWord)) {
                    q.push(newWord);
                    m[newWord] = m[word] + 1;  //因为m[word]等于q.front(),先进入队列word对应的m[word]的value一定是所有能转换到endWord路径最短的值。
                }
            }
        }
    }
    return 0;
}

**

Word Ladder II

**
Given two words (beginWord and endWord), and a dictionary’s word list, find all shortest transformation sequence(s) from beginWord to endWord, such that:

Only one letter can be changed at a time
Each transformed word must exist in the word list. Note that beginWord is not a transformed word.
For example,

Given:
beginWord = “hit”
endWord = “cog”
wordList = [“hot”,”dot”,”dog”,”lot”,”log”,”cog”]
Return
[
[“hit”,”hot”,”dot”,”dog”,”cog”],
[“hit”,”hot”,”lot”,”log”,”cog”]
]
Note:
Return an empty list if there is no such transformation sequence.
All words have the same length.
All words contain only lowercase alphabetic characters.
You may assume no duplicates in the word list.
You may assume beginWord and endWord are non-empty and are not the same.

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

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

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

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

//#include<stdio.h>
//#include<string.h>
#include<iostream>
#include<algorithm>
#include<sstream>
#include<string>
#include<vector>
#include<map>
#include<unordered_set>
#include<unordered_map>
using namespace std;

void fun(string start, string end, unordered_map<string, unordered_set<string>> path, vector<vector<string>>& ans, vector<string> vec) {
    vec.push_back(start);
    if (start == end) {
        reverse(vec.begin(), vec.end());
        ans.push_back(vec);
        return;
    }
    for (auto it = path[start].begin(); it != path[start].end(); ++it) {
        fun(*it, end, path, ans, vec);
    }
    return;
}


vector<vector<string>> findLadders(string start, string end, unordered_set<string> &dict) {
    vector<vector<string>> ans;
    if (dict.empty())
        return ans;
    unordered_map<string, unordered_set<string>> path;
//  dict.insert(end);
    vector<string>pre;
    vector<string>curr;
    unordered_set<string> unvisited = dict;
    if (dict.count(start) > 0)
        unvisited.erase(start);
    pre.push_back(start);
    unordered_map<string, int> m;
    while (m[end] <= 0 && unvisited.size() > 0) {
        while (!pre.empty()) {
            string word = pre.back();
            pre.pop_back();
            for (int i = 0; i < word.size(); ++i) {
                string newWord = word;
                for (char aa = 'a'; aa <= 'z'; ++aa) {
                    newWord[i] = aa;
                    if (newWord == word)
                        continue;
                    if (unvisited.count(newWord) > 0) {
                        ++m[newWord];
                        curr.push_back(newWord);
                        path[newWord].insert(word);  //构建反向邻接表
                    }
                }
            }
        }
        if (curr.empty())
            break;
        for (auto it : curr)
            unvisited.erase(it);
        pre = curr;
        curr.clear();
    }
    vector<string>vec;
    fun(end, start, path, ans, vec);
    return ans;
}
int main() {
    vector<vector<string>> ans;
    string start = "hit";
    string end = "cog";
    unordered_set<string> dict = { "hot","dot","dog","lot","log","cog" };
    ans = findLadders(start, end, dict);
    system("pause");
    return 0;
}

参考文献
leetcode 解题报告 Word Ladder II

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值