leetcode 解題報告 Word Ladder II


題目不多說了。見https://oj.leetcode.com/problems/word-ladder-ii/

這一題我反復修改了兩天半。嘗試過各種思路,總是報TLE。終於知道這一題為什么是leetcode上通過率最低的一道題了,它對時限的要求實在太苛刻了。

在我AC版本代碼的前一個版本,最好也就過了單詞長度為7的test case。然后就TLE了。

到底問題在哪兒?我從算法,STL數據結構,代碼優化各種角度思考。比較可惜的是,直到最后我也沒有弄清為啥能AC,為啥會TLE。(都是我寫的代碼,都是我的思路,太詭異了。。。)

但不管如何,通過這一題,學到的還真是挺多。這里總結下吧。

拿到這一題的時候,首先想到的就是爆搜。依次替換單詞中的字母,然后依次為基礎進行搜索。

是BFS還是DFS呢?

先引用下Stack Overflow上的兩個解答

That heavily depends on the structure of the search tree and the number and location of solutions. If you know a solution is not far from the root of the tree, a breadth first search (BFS) might be better. If the tree is very deep and solutions are rare, depth first search (DFS) might take an extremely long time, but BFS could be faster. If the tree is very wide, a BFS might need too much memory, so it might be completely impractical. If solutions are frequent but located deep in the tree, BFS could be impractical. If the search tree is very deep you will need to restrict the search depth for depth first search (DFS), anyway (for example with iterative deepening).

--------------------------------------------------------------------------------------

BFS is going to use more memory depending on the branching factor... however, BFS is a complete algorithm... meaning if you are using it to search for something in the lowest depth possible, BFS will give you the optimal solution. BFS space complexity is O(b^d)... the branching factor raised to the depth (can be A LOT of memory).

DFS on the other hand, is much better about space however it may find a suboptimal solution. Meaning, if you are just searching for a path from one vertex to another, you may find the suboptimal solution (and stop there) before you find the real shortest path. DFS space complexity is O(|V|)... meaning that the most memory it can take up is the longest possible path.

They have the same time complexity.

其實這一題很容易在腦海匯中勾勒一下DFS/BFS搜索樹的大致樣子。

如果選用DFS(即廣義上的爆搜遞歸)

void search(string &word, string &end, unordered_set<string> &dict, int level)
{
     if(word == end)
         return;
     
     if( level == dict.size())
         return;
     
     for(int i = 0; i < word.length(); i++)
    {
           for(int ch = 'a'; j <='z'; j++)
           {
                    string tmp = word;
                    if(tmp[i] == ch)
                             continue;
                    tmp[i] = ch;
                    if(dict.count(tmp) > 0)
                          search(tmp, end, dict, level+1);                  
            }        
    }            

如此,必須要遍歷整棵搜索樹,記錄所有可能的解路徑,然后比較最短的輸出,重復節點很多,時間復雜度相當大。有人問可以剪枝么,答案是這里沒法剪。如果把已經訪問過的剪掉,那么就會出現搜索不完全的情況。

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

這樣看,如果將相鄰的兩個單詞(即只差一個字母的單詞)相互連在一起,這就是一個圖嘛。經典的圖算法,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

說白了,構造一個反向鄰接表即可。

對了,還沒說整個程序的終止條件。如果找到了,把當前層搜完就退出。如果沒找到,字典遲早會被清空,這時候退出就行。

說了這么多,上代碼吧

 1 class Solution {
 2 public:
 3 vector<string> temp_path;
 4 vector<vector<string>> result_path;
 5 
 6 void GeneratePath(unordered_map<string, unordered_set<string>> &path, const string &start, const string &end)
 7 {
 8     temp_path.push_back(start);
 9     if(start == end)
10     {
11         vector<string> ret = temp_path;
12         reverse(ret.begin(),ret.end());
13         result_path.push_back(ret);
14         return;
15     }
16 
17     for(auto it = path[start].begin(); it != path[start].end(); ++it)
18     {
19             GeneratePath(path, *it, end);
20             temp_path.pop_back();
21     }
22 }
23 vector<vector<string>> findLadders(string start, string end, unordered_set<string> &dict)
24 {
25     temp_path.clear();
26     result_path.clear();
27 
28     unordered_set<string> current_step;
29     unordered_set<string> next_step;
30 
31     unordered_map<string, unordered_set<string>> path;
32 
33     unordered_set<string> unvisited = dict;
34     
35     if(unvisited.count(start) > 0)
36         unvisited.erase(start);
37     
38     current_step.insert(start);
39 
40     while( current_step.count(end) == 0 && unvisited.size() > 0 )
41     {
42         for(auto pcur = current_step.begin(); pcur != current_step.end(); ++pcur)
43         {
44             string word = *pcur;
45 
46             for(int i = 0; i < start.length(); ++i)
47             {
48                 for(int j = 0; j < 26; j++)
49                 {
50                     string tmp = word;
51                     if( tmp[i] == 'a' + j )
52                         continue;
53                     tmp[i] = 'a' + j;
54                     if( unvisited.count(tmp) > 0 )
55                     {
56                         next_step.insert(tmp);
57                         path[tmp].insert(word);
58                     }
59                 }
60             }
61         }
62 
63         if(next_step.empty()) break;
64         for(auto it = next_step.begin() ; it != next_step.end(); ++it)
65         {
66             unvisited.erase(*it);
67         }
68 
69         current_step = next_step;
70         next_step.clear();
71     }
72     
73     if(current_step.count(end) > 0)
74         GeneratePath(path, end, start);
75 
76     return result_path;
77 }
78 };

 此外,這里還有一份代碼,寫的比較亂,但用的傳統隊列的思想,用兩個標記變量來指示層數的變化。也AC了。

class Solution {
public:
vector<vector<string>> output;
vector<string> cur;

void FindPath(unordered_map<string, unordered_set<string>> &graph, const string &start, const string &end)
{
    cur.push_back(start);
    if(start == end)
    {
        vector<string> ret = cur;
        reverse(ret.begin(),ret.end());
        output.push_back(ret);
        return;
    }

    for(auto it2 = graph[start].begin(); it2 != graph[start].end(); ++it2)
    {
            FindPath(graph, *it2, end);
            cur.pop_back();
    }
}


vector<vector<string>> findLadders(string start, string end, unordered_set<string> & _dict)
{
    unordered_set<string> dict = _dict;
    if(dict.count(start) >0)
        dict.erase(start);

    output.clear();
    cur.clear();
    
    unordered_map<string, unordered_set<string>> graph;
    queue<string> q;
    unordered_map<string, int> depth;
    
    q.push(start);
    depth[start] = 0;
    
    bool found = false;
    
    int cur_deep = 0;
    int pre_deep = 0;

    while(!q.empty())
    {

        string word = q.front();
        q.pop();
        
        pre_deep = cur_deep;
        cur_deep = depth[word];

        if(pre_deep != cur_deep)
        {
            if(depth.count(end) > 0)
            {
                found = true;
                break;
            }
            else if(depth.size() == dict.size() + 1)
                break;
        }


        for( int i = 0; i < start.length(); ++i)
        {
            for(char ch = 'a'; ch <= 'z'; ch++)
            {
                string tmp = word;
                if(tmp[i] != ch)
                {
                    tmp[i] = ch;
                    
                    int t = depth.count(tmp);
                    if((t == 0 && dict.count(tmp) > 0) || (t > 0 && depth[tmp] == cur_deep + 1) )
                    {
                             graph[tmp].insert(word);
if(t == 0)
{ q.push(tmp);
depth[tmp] = cur_deep + 1;
} } } } } }
if(found) { FindPath(graph, end, start); } return output; } };

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM