《算法設計手冊》面試題解答 第五章:圖的遍歷 附:DFS應用之找掛接點

第五章面試題解答

5-31.

  DFS和BFS使用了哪些數據結構?

解析:

  其實剛讀完這一章,我一開始想到的是用鄰接表來表示圖,但其實用鄰接矩陣也能實現啊?後來才發現應該回答,BFS用隊列實現;DFS可以用棧實現也可以改寫成遞歸形式。用棧來消除遞歸改寫DFS也出現在《算法導論》的練習題22.3-6。

 

5-32.

  寫一個函數,在遍歷二叉查找數的時候,輸出第i個結點。

解析:

  模仿DFS遍歷時維護一個進入時間數組和完成時間數組的特點,維護一個全局變量n,在中序遍歷的時候,每遍歷一個結點就n++,直到n=i時打印這個結點,或者遍歷完成時仍然n!=i時報錯即可。

 

題外話:

  第5章“圖遍歷”的Interview Problems部分確實只有這兩題,而第6章“帶權圖算法”乾脆就沒Interview Problems這一部分。其實圖本身的表示就比較複雜,幾個基本的圖算法雖然思路不難,但是代碼量不小,同時要寫繁瑣的初始化方法,寫具體算法實現時還要用到各種輔助數據結構,編碼起來想少都不行。況且就算真寫出來了,正確性的證明又很費功夫(比如拓撲排序、強聯通分支),因此面試時除了專門做這個方向的,很少會考到具體的代碼書寫,更不用說其他變形、改進了。這也就是爲什麼圖相關的面試題並不多的原因。

  另外提一下,《算法設計手冊》上的拓撲排序和強聯通分支算法是基於邊分類的,而且它把DFS寫成了一個可擴充的框架;而《算法導論》則是利用最後完成時間來實現這兩個算法,在此之前把DFS寫成了一個子程序供這兩個算法調用。究竟孰優孰劣我不評價,從先入爲主和對我而言易於理解的角度和來說,我更傾向於使用後者。

 

DFS應用之找掛接點(Articulation Vertices,《算法導論》中文版的翻譯) 

  既然提到了《算法設計手冊》上DFS的框架寫法了,這個算法正好來進行演示。(《算法導論》思考題22-2曾提到了這個概念)。

  先來看看《算法設計手冊》版DFS框架:

//圖用鄰接表實現
//entry_time[]  某結點開始處理的時間
//exit_time[]    某結點處理完畢的時間
//discoverd[]    某個結點是否已被發現
//process_vertex_early() 某個結點剛發現時採取的處理
//process_edge() 對邊的處理
//process_vertex_late() 某個結點所有鄰接邊處理完後的動作
//以上三個函數決定了DFS的行爲,如果只需要基本的功能,可以實現爲空操作,或者輸出該結點/邊用於追蹤遍歷過程

dfs(graph *g, int v)
{
    edgenode *p; /* temporary pointer */
    int y; /* successor vertex */
    if (finished) return; /* allow for search termination */

    discovered[v] = TRUE;
    time = time + 1;
    entry_time[v] = time;
    process_vertex_early(v);
    p = g->edges[v];
    while (p != NULL) {
          y= p->y;
          if (discovered[y] == FALSE) {
               parent[y] = v;
               process_edge(v,y);
               dfs(g,y);
          }
          else if ((!processed[y]) || (g->directed))
               process_edge(v,y);
           if (finished) return;
               p = p->next;
     }
     process_vertex_late(v);
     time = time + 1;
     exit_time[v] = time;
     processed[v] = TRUE;
}    
《算法設計手冊》版DFS框架

  掛接點是指,如果我們從連通圖中刪除這個結點,會導致圖不再連通。下圖中的白點就是掛接點,可以把它看作爲圖上最脆弱的點。

 

  使用DFS或BFS寫一個暴力算法很簡單:刪除一個結點,用DFS或BFS判斷是否連通;恢復原圖,刪除下一個結點繼續判斷,直至所有接點都判斷過。如果結點數n個,邊數m個,暴力算法時間複雜度爲O(n(m+n))。

  現在用DFS遍歷時生成樹的角度來看。對於這棵樹上所有在原圖的邊,歸爲TREE邊;其餘所有邊是BACK邊,即它們指向一個先於這個結點遍歷的另一個結點。

  可以發現一些規律:

DFS樹的葉結點不可能是掛接點,刪去它樹的連通性未被破壞。只有樹的內結點可能是掛接點。

對於DFS樹的根,如果它只有一個孩子,那麼刪去它和刪去一個葉結點是一樣的。而孩子多於1個時,刪去根會導致孩子們不再連通,也即它是掛接點。 

對於一個BACK邊,它連接的兩個結點的TREE路徑(即DFS時形成的路徑)上的所有結點都不可能是掛接點。

  尋找掛接點需要維護BACK邊連接DFS樹上結點與其祖先的信息。用reachable_ancesor[v]表示結點v用BACK邊能連接的最老祖先(初始化爲v),tree_out_degree[v]表示結點在DFS樹的出度。edge_classification(int x,int y)用於判斷(x,y)是TREE還是BACK。

int reachable_ancestor[MAXV+1]; /* earliest reachable ancestor of v */
int tree_out_degree[MAXV+1]; /* DFS tree outdegree of v */
process_vertex_early(int v)
{
    reachable_ancestor[v] = v;
}

process_edge(int x, int y)
{
    int class; /* edge class */
    class = edge_classification(x,y);
    if (class == TREE)
        tree_out_degree[x] = tree_out_degree[x] + 1;
    if ((class == BACK) && (parent[x] != y)) {
        if (entry_time[y] < entry_time[ reachable_ancestor[x] ] )
            reachable_ancestor[x] = y;
    }
}

int edge_classification(int x, int y)
{
    if (parent[y] == x) 
        return TREE;
    else
        return BACK;
}

  下面是v與祖先的連通性和v是否是掛接點的關係,一共是三種情況:

  用代碼實現在process_vertex_late()裏,即:

process_vertex_late(int v)
{
    bool root; /* is the vertex the root of the DFS tree? */
    int time_v; /* earliest reachable time for v */
    int time_parent; /* earliest reachable time for parent[v] */
    if (parent[v] < 1) { /* test if v is the root */
        if (tree_out_degree[v] > 1)
            printf("root articulation vertex: %d \n",v);
        return;
    }

    root = (parent[parent[v]] < 1); /* is parent[v] the root? */
    if ((reachable_ancestor[v] == parent[v]) && (!root))
        printf("parent articulation vertex: %d \n",parent[v]);

    if (reachable_ancestor[v] == v) {
        printf("bridge articulation vertex: %d \n",parent[v]);
        if (tree_out_degree[v] > 0) /* test if v is not a leaf */
            printf("bridge articulation vertex: %d \n",v);
    }

    time_v = entry_time[reachable_ancestor[v]];
    time_parent = entry_time[ reachable_ancestor[parent[v]] ];
    if (time_v < time_parent)
        reachable_ancestor[parent[v]] = reachable_ancestor[v];
}

   最後幾行用entry_time[v]表示v的年齡,time_v是v通過BACK邊達到的最老結點。如果v的parent能通過v的BACK到達v的最老祖先,那麼parent(v)肯定不是掛接點,下次處理parent(v)時做出這樣的標記讓它能通過v的BACK到達v的最老祖先。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章