LeetCode 126. Word Ladder II

题目

题目给定两个单词(start和end)和由一组单词组成的词典,要求找到从start到end所有的变形序列,满足:

  • 每次只能改变一个字母;
  • 变形序列中的所有单词必须在词典中存在

举例来说,

start = "hit"
end = "cog"
dict = ["hot","dot","dog","lot","log"]

返回

[
   ["hit","hot","dot","dog","cog"],
   ["hit","hot","lot","log","cog"]
]

分析

挑战一下Hard的题目,通过率很低只有13.9%,题目给出了一些节点和构成图的方法,相当于给出了一个图,如果只需要求出其中的一条最短路,很显然用Dijkstra算法就能轻松解决,但是题目要求的是给出所有可能的变形序列,就需要特殊的解决方法。

思路上讲,Dijkstra算法可以看做是一种特殊的BFS,那么想求出所有路径很自然就想到使用BFS,使用类似穷举的方法,直到从start走到end。因为需要所有的路径,使用一个vector存储pre数组肯定不够,我选择使用一个由set组成的vector的来存储nexts,将由一个顶点能到达的所有顶点都放在对应的nexts中,然后nexts相当于组成了一个简化的图,包含所有可能的路径(有很多无效的路径),这时再经过一次DFS得出所有路径,方法类似于我的上一篇博客《Leetcode 526 357》中的做法,使用DFS穷举和判断,从start开始提取出所有能到达next的ladders。

实现

在具体实现上,我选择先把整个图构建出来,因为假设共有n个单词(包括start),每个单词都有m个字母,这样共需要m[n(n-1)/2]次字母间的比较,题目给出的n和m应该不会太大,这个复杂度可以接受,相比边BFS边建图,这样的做法思路更清晰,理解性更强,缺点是牺牲了空间。

开始实现后,我发现本题的主要问题还来自超时(Time Limit Exceed)和超出内存限制(Memory Limit Exceed),如何进行有效的时空的优化也是本题的难点。

空间优化

string类型毕竟比较大,相互比较也会很耗时,如果直接放进set或map中显然又费空间又费时间,整个图的结构会显得很重。我选择把string存储在一个vector中,再用一个map来记录一个string的索引值,这样不论是根据索引值找string,还是根据string找索引值都很快。在保存的图结构中,只需要存储索引值之间的关系即可,所有BFS和DFS的过程都不需要和真正的string打交道,所有的判断也只需要简单地在int之间进行。

这样的做法能节省相当的空间,很多东西用vector就能实现,时间上也会好一些,在编码上,除了构建图的过程,其他的代码都会简单一些,不必担心map抛出异常的处理。

时间优化

时间上的优化主要集中在如何减少BFS的情况,相应的也就能减少nexts的大小,显然也就优化了DFS的时间。

众所周知,BFS过程在一个图的每一层进行,在这个简单的无权无向图中,层是按照远离start的方向,按照距离start的距离区分,距离相等的顶点是一层。那么问题就集中在当前正在访问的层和下一层之间,当前层的距离一定低于下一层,那么如果一个顶点在当前层里出现,那它在下一层中就没有出现的必要,因为无论如何,在下一层出现一定会产生一个效果等价但长度更长的路径,这是没有必要的。如果删除这些,那么DFS时也能避免搜索很多无效路径,产生额外的好处。

常规的BFS并不区分当前层和下一层,只用一个队列将所有节点入队,而我的做法是使用两个队列,使用curr_queue指针指向当前正在访问的队列,使用next_queue指针来指向访问时新添加的顶点的队列,在curr_queue被访问完变成空时,再把curr_queue指针和next_queue指针的值交换,curr_queue就变成了完整的当前层队列,而next_queue变成空,这里就可以将curr_queue中的所有节点标记为visited,下次添加节点到next_queue时就不会添加curr_queue中的节点进去。

这样做也有一个额外的好处,当在当前层搜索到end的时候,next_queue就可以很方便地直接宣布作废(不再交换),只把当前层搜索完即可。

实现上,因为STL中的queue不能访问其中的内容,这里只能选择STL中的deque来实现,刚才提到使用指针来实现显式地切换当前层和下一层,只需要交换指针指向的deque,不需要任何实际的复制操作,时间上没有增加什么复杂度,空间上也只是将一个长queue分成两个,也没有增加什么复杂度,额外的复杂度主要来自选用deque实现队列,增加了存储queue的空间和添加删除操作的时间。

代码

#include 
#include 
#include 

#include 
#include 

#include 
#include 
#include 
#include 

using namespace std;

template<class T>
void print2DMatrix(const vector<vector >& matrix) {
    for (unsigned int i = 0; i < matrix.size(); ++i) {
        if (!matrix[i].empty())
            cout << matrix[i].front();
        for (unsigned int j = 1; j < matrix[i].size(); ++j)
            cout << " " << matrix[i][j];
        cout << endl;
    }
}

template <class InputIterator>
void printContainer(InputIterator first, InputIterator last) {
    if (first != last)
        cout << *first++;
    while (first != last)
        cout << " " << *first++;
    cout << endl;
}

template<class Vex>
class Graph {
  public:
    vector<unordered_set<int> > adj;
    vector VLookup;
    unordered_mapint> VIndexLookup;

    int getVexIndex(const Vex& v) const {
        assert(VIndexLookup.count(v) != 0);
        return VIndexLookup.at(v);
    }

    inline unsigned int size() const {
        return VLookup.size();
    }

    bool addVex(const Vex& v) {
        if (VIndexLookup.count(v) != 0)
            return false;

        VIndexLookup[v] = VLookup.size();
        VLookup.push_back(v);
        adj.push_back(unordered_set<int>());

        return true;
    }

    void addEdge(const Vex& start, const Vex& end) {
        const int startIndex = getVexIndex(start);
        const int endIndex = getVexIndex(end);

        adj[startIndex].insert(endIndex);
    }

    void print() const {
        for (unsigned int u = 0; u < size(); ++u) {
            cout << VLookup[u] << ": ";
            for (const int v : adj[u])
                cout << VLookup[v] << " ";
            cout << endl;
        }
    }

};

class Solution {

    bool BFS(const Graph<string>& G, const int startIndex, const int endIndex,
             vector<unordered_set<int> >& nexts) {

        nexts.assign(G.size(), unordered_set<int>());

        bool isFound = false;
        vector<bool> visited(G.size(), false);

        deque<int> bfs_queue[2];
        deque<int>* curr_queue = &bfs_queue[0];
        deque<int>* next_queue = &bfs_queue[1];

        curr_queue->push_back(startIndex);
        visited[startIndex] = true;

        while (!curr_queue->empty()) {
            const int u = curr_queue->front();
            /* visit bfs_queue.front()*/
            if (u == endIndex) {
                isFound = true;
                // cout << "found" << endl;
            }

            if (!isFound) {
                for (const int v : G.adj[u]) {
                    if (!visited[v]) {
                        next_queue->push_back(v);
                        nexts[u].insert(v);
                    }
                }
            }

            curr_queue->pop_front();
            if (curr_queue->empty() && !isFound) {
                swap(curr_queue, next_queue);
                for (const int i : *curr_queue)
                    visited[i] = true;
            }
        }

        // for (const auto& s : nexts)
        //     printContainer(s.begin(), s.end());

        return isFound;
    }

    void DFS(const int startIndex, const int endIndex,
             const vector<unordered_set<int> >& nexts,
             vector<int>& path, vector<vector<int> >& paths) {
        if (startIndex == endIndex)
            paths.push_back(path);
        else {
            for (const int idx : nexts[startIndex]) {
                path.push_back(idx);
                DFS(idx, endIndex, nexts, path, paths);
                path.pop_back();
            }
        }
    }

  public:
    vector<vector<string>> findLadders(string beginWord, string endWord, vector<string>& wordList) {

        vector<vector<string> > ladders;

        if (find(wordList.begin(), wordList.end(), endWord) == wordList.end())
            return ladders;

        Graph<string> G;
        G.addVex(beginWord);
        for (const string& w : wordList)
            G.addVex(w);

        for (unsigned int i = 0; i < G.VLookup.size(); ++i) {
            for (unsigned int j = i + 1; j < G.VLookup.size(); ++j) {
                const string& w1 = G.VLookup[i];
                const string& w2 = G.VLookup[j];
                int dist = 0;
                for (unsigned int k = 0; k < w1.size(); ++k)
                    if (w1[k] != w2[k])
                        dist++;
                if (dist == 1) {
                    G.addEdge(w1, w2);
                    G.addEdge(w2, w1);
                }
            }
        }

        G.print();

        const int startIndex = G.getVexIndex(beginWord);
        const int endIndex = G.getVexIndex(endWord);

        vector<unordered_set<int> > nexts;
        bool isFound = BFS(G, startIndex, endIndex, nexts);
        if (!isFound)
            return ladders;

        vector<int> path;
        vector<vector<int> > paths;

        path.push_back(startIndex);
        DFS(startIndex, endIndex, nexts, path, paths);

        for (const auto& path : paths) {
            vector<string> v;
            for (const int i : path)
                v.push_back(G.VLookup[i]);
            ladders.push_back(v);
        }

        return ladders;
    }
};

int main(int argc, char const *argv[]) {
    Solution s;

    // string beginWord = "hit";
    // string endWord = "cog";

    // vector wordList({"hot", "dot", "dog", "lot", "log", "cog"});

    string beginWord = "qa";
    string endWord = "sq";

    vector<string> wordList({ "si", "go", "se", "cm", "so", "ph", "mt", "db", "mb", "sb", "kr", "ln",
                              "tm", "le", "av", "sm", "ar", "ci", "ca", "br", "ti", "ba", "to", "ra",
                              "fa", "yo", "ow", "sn", "ya", "cr", "po", "fe", "ho", "ma", "re", "or",
                              "rn", "au", "ur", "rh", "sr", "tc", "lt", "lo", "as", "fr", "nb", "yb",
                              "if", "pb", "ge", "th", "pm", "rb", "sh", "co", "ga", "li", "ha", "hz",
                              "no", "bi", "di", "hi", "qa", "pi", "os", "uh", "wm", "an", "me", "mo",
                              "na", "la", "st", "er", "sc", "ne", "mn", "mi", "am", "ex", "pt", "io",
                              "be", "fm", "ta", "tb", "ni", "mr", "pa", "he", "lr", "sq", "ye"
                            });

    vector<vector<string> > ladders;
    ladders = s.findLadders(beginWord, endWord, wordList);

    print2DMatrix(ladders);

    system("pause");
    return 0;
}

代码包含测试使用的main函数和打印函数,还有一个使用template编写的Graph辅助类,用于构建无权图。

感想

第一次在LeetCode上交这么长的代码,去除main函数和打印相关的函数,最后提交了159行代码,最后运行时间是369ms,解决了问题。回顾来看,思路和优化技巧都很直接,看到答案中的大神有使用双向BFS解题的,应该会快不少,觉得佩服佩服,还需要继续努力。

你可能感兴趣的:(算法设计与分析,leetcode)