Leetcode - Word Ladder I,II

首先须要说明的是,这两道题表明了一种思想或者说一种思惟定式
对比说明了dfs vs bfs 有关效率,通用场景和局限性的优劣分析,详细见具体讲解segmentfault


Attention:
本文介绍的思路参考了YouTube上一位讲解leetcode题目的热心刷友
Leetcode - 127 Word Ladder YouTube连接
Leetcode - 126 Word Ladder II YouTube连接
讲解风格图文并茂,很是生动,在刷leetcode的过程当中给予了我不少帮助。有兴趣的能够关注一下数据结构

Leetcode - 127 Word Ladder
本题是比较常规的bfs类型题目,不作过多讲解。
须要注意的地方有:
1.不一样于Binary Tree的bfs,由于二叉树能够视为有向图,只能从由root 拓展到child,因此不须要已访问信息,而本题是无向图,任意的s1能够拓展到s2,那么必有s2也能够拓展到s1,须要额外的信息记录bfs的visited状态
2.str_start 不在wordlist里面,加入到queue以后不须要标注已访问状态
3.咱们不是每次取出一个str,而后在整个list中逐个取出s,计算两个str 的 dis == 1,这样的时间复杂度是O(N^2),随着list的增加会变得愈来愈难以承受直至TLE
Leetcode - Word Search中提到"All words contain only lowercase alphabetic characters.",与本题的同样。这就说明了咱们能够用一种更smart的方法求得str能够跳转的集合,具体的操做过程见getNextstrs函数函数

class Solution {
public:
    // 路径中的相邻的str有且只有一个位置的char不一样
    unordered_set<string> getNextstrs(const unordered_set<string> & exist,const string str)
    {
        unordered_set<string> ans;
        int lens = str.length();
        for (int i = 0; i < lens; ++i)
        {
            char ch = str[i];
            for (char c = 'a'; c <= 'z'; ++c)
            {
                if (c == ch)
                    continue;
                string curs = str;
                curs[i] = c;
                if (exist.find(curs) != exist.end())
                {
                    ans.insert(curs);
                }
            }
        }
        return ans;
    }
    // 构建hash,相似于二叉树中获取左右子节点
    void Init(unordered_map<string,unordered_set<string>> & mmp,const unordered_set<string> & exist, 
        vector<string> & wordlist, const string & beginword)
    {
        for (auto curstr : wordlist)
        {
            mmp[curstr] = getNextstrs(exist,curstr);
        }
        // beginword 不在list中,须要额外求其下一层的可到达位置
        mmp[beginword] = getNextstrs(exist,beginword);
    }

    int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
        unordered_map<string, unordered_set<string>> hashmap;
        unordered_set<string> exist;
        for (auto str : wordList)
            exist.insert(str);
        Init(hashmap, exist, wordList, beginWord);
        queue<string> qu;
        qu.push(beginWord);
        int curstep = 0;
        // 用done来记录已被访问的位置
        unordered_set<string> done;
        while (!qu.empty())
        {
            int cursize = qu.size();
            ++curstep;
            for (int i = 1; i <= cursize; ++i)
            {
                string curs = qu.front();
                if (curs == endWord)
                    return curstep;
                qu.pop();
                for (auto nextstr : hashmap[curs])
                {
                    if (done.find(nextstr) != done.end())
                        continue;
                    done.insert(nextstr);
                    qu.push(nextstr);
                }
            }
        }
        // 当退出while 循环意味着beginword 开始的路径不能到达endword
        return 0;
    }
};

Leetcode - 126. Word Ladder II测试

首先说明这道题很是有意思,咱们能够采用dfs + backtracing的方式快速肯定思路并code。
若是有掌握dfs + backtracing思想或看过前几篇拙做的话,你们在code以后顺利pass测试用例以后高高兴兴去submit一发,结果是让人蛋疼的TLE。
下面给出一组朴素的dfs会发生TLE的状况:
clipboard.png
那么为何会TLE?
这就回到了本文最开始的地方,BFS与DFS的区别究竟在哪儿?何时该用BFS,何时该用DFS,这确实是个好问题。我根据本身的认知给出我的见解:优化

BFS:
1.用来判断可行解的存在性问题(存在一个解,任务完成)
2.可行解的解空间的最小性问题(咱们会像Binary Tree 的BFS的过程,也是获得了一个path,BFS能够用来处理path的最小长度,Leetcode - 127 Word Ladder就是一个很好的例子)spa

DFS:
用来在所有的解空间中寻找全部的可行解(或许须要知足必定性质的可行解)code

即DFS侧重于解的完备性,BFS侧重解的存在性与长度最短(固然对于遍历数据结构这样不求解的过程其实没什么差别)ip

本题给出一组TLE数据就是为了详细阐释咱们优化朴素的dfs的出发点和方法
1.在O(n)的时间对list中的每个str,创建了一个hashmap,而后dfs的过程当中就不用带着整个list而是带着能够映射到的list的一个子集,这样缩小了搜索空间,这是第一重优化
2.注意到本题是须要咱们找到最短路径长度的全部路径集,然而问题是对于DFS而言,咱们不遍历完整个解空间是无法肯定minstep的,没有minstep就不用谈剪枝的问题了,因此咱们在DFS以前须要用BFS求一下最短路径的长度,而后在DFS的过程当中就能够用path的大小来剪枝,这是第二重优化ci

原本思路讲到这里就应该结束了,如上例所示,对于这组测试数据仍然是TLEleetcode


这是由于咱们作了不少无用功,剪枝还得继续优化的意思,朴素的理解就是当前路径长度大于minstep就剪枝这是一个弱条件,咱们还须要更强力的约束方案,约束咱们的解必定朝着endword的方向前进!

在YouTube上的解说里获得的启发是,在BFS的过程当中,记录每一个访问过的点距离起始点的dis
显然dis[beginword] = 0,dis[endword] = minstep - 1
而后逆向DFS(从终止点开始dfs),对于每一个每一个即将要拓展的点
(注意这里有个陷阱:list是拓展不到beginword的,由于beginword并不在list之中)
最短路径必然知足的条件是:distance + pathsize = minstep
其能拓展的条件是:
1.BFS在运行过程当中访问到,并获得了相对最短distance(边的长度)
2.distance是当前节点到起始点边的长度,path有个当前路径长度
当且二者之和大于minstep便可当即剪枝
3.根据注意,可行解的断定条件是pathsize = minstep - 1,转而判断distance是否为1,为1即为知足条件的可行解,不然剪枝
4.当且仅当distance + pathsize <= minstep 继续DFS调用下去,在此过程当中要设置当前路径的已访问状态

class Solution {
public:

    unordered_set<string> getNexts(const string & s, const unordered_set<string> &mst)
    {
        unordered_set<string> res;
        int lens = s.length();
        for (int i = 0; i < lens; ++i)
        {
            char ch = s[i];
            for (char c = 'a'; c <= 'z'; ++c)
            {
                if (c != ch)
                {
                    string bak = s;
                    bak[i] = c;
                    if (mst.find(bak) != mst.end())
                        res.insert(bak);
                }
            }
        }
        return res;
    }

    void InitMap(unordered_map<string, unordered_set<string>> &mmp, const unordered_set<string> &mst, const string &beginWord)
    {
        for (auto its : mst)
            mmp[its] = getNexts(its, mst);
        mmp[beginWord] = getNexts(beginWord, mst);
    }
    // mst 表明的是剩余的有效word
    int minLaddresLength(unordered_map<string, unordered_set<string>> &mmp, const unordered_set<string> &mst,
        const string &beginWord, const string &endWord,unordered_map<string,int> & dis)
    {
        unordered_set<string> curmst = mst;
        queue<string> qu;
        qu.push(beginWord);
        curmst.erase(beginWord);
        int minstep = 0;
        while (!qu.empty())
        {
            int cursize = qu.size();
            ++minstep;
            for (int i = 1; i <= cursize; ++i)
            {
                string curs = qu.front();
                qu.pop();
                dis[curs] = minstep - 1;
                if (curs == endWord)
                    return minstep;
                for (auto itr : mmp[curs])
                {
                    if (curmst.find(itr) != curmst.end())
                    {
                        qu.push(itr);
                        curmst.erase(itr);
                    }
                }
            }
        }
        return 0;
    }

    void dfs(vector<vector<string>> & vct,vector<string> & curpath,
        unordered_map<string, unordered_set<string>>& mmp, unordered_set<string> &mst,
        const string s,unordered_map<string,int> & dis,const int minstep)
    {
        if (curpath.size() >= minstep)
            return;
        string curs = curpath[curpath.size()  - 1];
        // 这里要先判断当前节点是否已经标记了到起始点的距离
        // 有标记的话distance >= 1 ,不然当前点必定不在最短路径上
        if (int(curpath.size()) == minstep - 1)
        {
            if (dis.count(curs) >= 1 && dis[curs] == 1)
            {
                curpath.push_back(s);
                vct.push_back(vector<string>(curpath.rbegin(),curpath.rend()));
                curpath.pop_back();
            }
            return;
        }
        
        if (dis.count(curs) <= 0)
            return;
        if (curpath.size() + dis[curs] > minstep)
            return;
        for (auto its : mmp[curs])
        {
            if (mst.find(its) != mst.end())
            {
                mst.erase(its);
                curpath.push_back(its);
                dfs(vct, curpath, mmp, mst, s, dis, minstep);
                curpath.pop_back();
                mst.insert(its);
            }
        }
        
    }

    vector<vector<string>> findLadders(string beginWord, string endWord, vector<string>& wordList) {
        unordered_map<string, unordered_set<string>> mmp;
        unordered_map<string, int> dis;
        unordered_set<string> mst(wordList.begin(), wordList.end());
        InitMap(mmp,mst,beginWord);
        int minPathLen = minLaddresLength(mmp,mst,beginWord,endWord,dis);
        // 逆向dfs,逆向剪枝更快
        vector<vector<string>> vct;
        vector<string> curpath;
        // 逆向遍历
        curpath.push_back(endWord);
        mst.erase(endWord);
        dfs(vct, curpath, mmp, mst, beginWord, dis, minPathLen);
        return vct;
    };
};

关于Leetcode - 126. Word Ladder II,我的认为没必要强求必定AC,这题主要是考察了DFS与BFS的选用条件,通常想到用BFS求得最短路径长度以一阶剪枝就不错了。逆向DFS配合BFS求得距离函数加快剪枝的思路这个思路要是没有处理过相似题目并有深刻总结怕是硬想是想不出来。这题新手慎重,主要是开拓思路多见识一些辅助配合的作法。

相关文章
相关标签/搜索