1. 程式人生 > >有向/無向圖中搜環

有向/無向圖中搜環

pan return 逆向 當我 操作 fat 連通 搜索 對比

  經常遇到一類問題,提供一個圖,判斷其中是否含環。所謂的環是一條起點與終點相同的路徑(至少含有一條邊,兩個結點)。由於不帶環的連通圖和帶環的連通圖有著本質的區別,不帶環的連通圖是樹,而樹相較於一般的圖可以支持更多更高效的算法,比如log2(n)時間復雜度內找任意兩點的路徑信息,在樹上進行樹形DP等等。

  圖按照邊是否有向可以分為有向圖和無向圖。在兩類圖中找環的時間復雜度均為O(n),而判斷是否含環的時間復雜度也是O(n),因此只陳述找環的方法。

無向圖找環

  無向圖找環,因為無向圖中沒有明確的根,我們可以令任意結點為根做DFS操作,每次搜索到一個結點,就修改其狀態為已訪問,直到搜索到已訪問的結點u,這時候通過退棧必定會碰到另外一個u結點,二者之間的路徑就是環。

findLoop(node, father, stack) //node為根結點,father設為空,stack用於記錄可能存在的環信息
    stack.push(node)
    if(node.visit)
        return true
    node.visit = true
    for child in node.children
        if(child == father)
            continue
        if(findLoop(child, node, stack))
            return true
    stack.pop()
    
return false

  說明正確性。很顯然如果我們第二次訪問某個結點,很顯然兩次訪問對應的路徑必定有相同的根結點(因為無向圖的原因DFS會搜索整個連通圖中的所有結點),由於DFS每次得到的路徑不同,因此我們得到了兩條起點相同終點相同的路徑,將兩條路徑首位相連,我們就得到了一個環。如果我們第二次訪問相同結點u,那麽當前路徑中必定包含u,因為在第一次搜索到u時,我們會繼續搜索其子樹,此時沿著第二條路徑逆向走,必定會抵達某個訪問過的結點(可能是根),那麽若這個結點不是u,就違背了u是第一個被二次訪問結點這一前提,故當前路徑中必定包含u,兩個結點之間的路徑中沒有重復結點,是一個簡單環。

  復雜度非常簡單,我們為每個未被訪問過的結點調用該方法,每次進入方法,或者修改結點的訪問狀態,或者找到環,而函數內部的邏輯(不含循環)是常數時間復雜度,循環最多發生|E|次,因此時間復雜度為O(|V|+|E|)。

有向圖找環

  有向圖找環相對比較復雜。由於圖未必連通(可能由若幹連通子圖構成),我們需要建立一個公共的根結點r,並從r向圖中所有結點建立一條單向邊(由於r只有出邊,因此r必定不是環的一部分,即不影響我們找到的環)。之後從r出發進行DFS。同樣我們需要增加訪問狀態來避免重復搜索,但是訪問兩次的結點未必構成環,比如考慮兩條路徑r->a->b與r->b,很顯然a與b之間未必構成環。我們還需要加入一個在棧標誌instk,為true表示這個結點在當前路徑上,false表示不在。只有搜到的二次訪問結點滿足該結點在棧中,才能保證其處於環上。  

findLoop(node, stack)
    if node.visit
        if node.instk == false
            return false
        stack.push(node)
        return true;
    node.visit = true
    node.instk = true
    stack.push(node)
    
    for child in node.children
        if(findLoop(child, stack))
            return true
    
    node.instk = false
    stack.pop()
    return false

  如果圖中確實含環,即存在路徑u->...->u,那麽當我們訪問到環上的任意結點u時,由於DFS的緣由,必定會回到自身或是找到另外一個環並退出,無論哪種情況我們都找到了一個環。而如果第一次發現一個結點u被二次訪問且在棧中,那麽由於該結點在棧中,那麽路徑中必然包含u,二者之間的路徑則形成了環,而由於路徑中只含有訪問一次的結點(除了u),因此找到的環是簡單環。

  時間復雜度與無向圖的一致,也是O(|V|+|E|)。

有向/無向圖中搜環