首先須要說明的是,這兩道題表明了一種思想或者說一種思惟定式
對比說明了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的狀況:
那麼爲何會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求得距離函數加快剪枝的思路這個思路要是沒有處理過相似題目並有深刻總結怕是硬想是想不出來。這題新手慎重,主要是開拓思路多見識一些輔助配合的作法。