1

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


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就是一个很好的例子)

DFS:
用来在全部的解空间中寻找所有的可行解(或许需要满足一定性质的可行解)

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

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

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


这是因为我们做了很多无用功,剪枝还得继续优化的意思,朴素的理解就是当前路径长度大于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求得距离函数加快剪枝的思路这个思路要是没有处理过类似题目并有深入总结怕是硬想是想不出来。这题新手慎重,主要是开拓思路多见识一些辅助配合的做法。


焚香谷主
4 声望0 粉丝