有向圖的拓撲排序——DFS

YVVT_Real發表於2022-12-30

有向圖的拓撲排序——BFS這篇文章中,介紹了有向圖的拓撲排序的定義以及使用廣度優先搜尋(BFS)對有向圖進行拓撲排序的方法,這裡再介紹另一種方法:深度優先搜尋(DFS)。

演算法

考慮下面這張圖:

img

首先,我們需要維護一個棧,用來存放DFS到的節點。另外規定每個節點有兩個狀態:未訪問(這裡用藍綠色表示)、已訪問(這裡用黑色表示)。

任選一個節點開始DFS,比如這裡就從0開始吧。

img

首先將節點0的狀態設為已訪問,然後節點0的鄰居(節點0的出邊指向的節點)共有1個:節點2,它是未訪問狀態,於是順下去訪問節點2。

img

節點2的狀態也設為已訪問。節點2有3個鄰居:3、4、5,都是未訪問狀態,不妨從3開始。一直這樣訪問下去,直到訪問到沒有出邊的節點7。

img

節點7沒有出邊了,這時候就將節點7入棧。

img

退回到節點6,雖然6還有鄰居,但是唯一的鄰居節點7是已訪問狀態,也入棧。再次退回,節點4的兩個鄰居也都已訪問,依舊入棧並後退。以此類推,退回到節點2。

img

節點2有3個鄰居,其中節點3和4已訪問,但是節點5還未訪問,訪問節點5。

img

接下來的步驟是一樣的,不再贅述了,直接退回到節點0並將0入棧。

img

現在,從節點0開始的DFS宣告結束,但是圖中還有未訪問的節點:節點1,從節點1繼續開始DFS。

img

節點1的鄰居節點2已經訪問過了,直接將節點1入棧。

img

至此,整個DFS過程宣告結束。從棧頂到棧底的節點序列1 0 2 5 3 4 6 7就是整個圖的一個拓撲排序序列。

實現

這裡同樣使用型別別名node_t代表節點序號unsigned long long

using node_t = unsigned long long;

同樣使用鄰接表來儲存圖結構,整個圖的定義如下:

class Graph {
    unsigned long long n;
    vector<vector<node_t>> adj;

protected:
    void dfs(node_t cur, vector<bool> &visited, stack<node_t> &nodeStack);

public:
    Graph(initializer_list<initializer_list<node_t>> list) : n(list.size()), adj({}) {
        for (auto &l : list) {
            adj.emplace_back(l);
        }
    }

    vector<node_t> toposortDfs();
};

DFS

函式dfs的引數及說明如下:

  • cur:當前訪問的節點。
  • visited:存放各個節點狀態的陣列,false表示未訪問,true表示已訪問。初始化為全為false
  • nodeStack:存放節點的棧。

整個過程如下:

  1. 首先,我們需要將當前節點的狀態設為已訪問:
visited[cur] = true;
  1. 依次檢查當前節點的所有鄰居的狀態:
for (node_t neighbor: adj[cur]) {
    // ...
}
  1. 如果某個節點已訪問,則跳過。
if (visited[neighbor]) continue;
  1. 否則,遞迴的對該節點進行DFS:
dfs(neighbor, visited, nodeStack);
  1. 所有鄰居檢查完後,就將該節點入棧:
nodeStack.push(cur);

整個dfs函式的程式碼如下:

void Graph::dfs(node_t cur, vector<bool> &visited, stack<node_t> &nodeStack) {
    visited[cur] = true;
    for (node_t neighbor: adj[cur]) {
        if (visited[neighbor]) continue;
        dfs(neighbor, visited, nodeStack);
    }
    nodeStack.push(cur);
}

拓撲排序

我們需要初始化3個資料結構:

  • sort:存放拓撲排序序列的陣列。
  • visited:見上文。
  • nodeStack:見上文。
vector<node_t> sort;
vector<bool> visited(n, false);
stack<node_t> nodeStack;

整個過程如下:

  1. 依次檢查每個節點的狀態,如果未訪問,則從該節點開始進行DFS:
for (node_t node = 0; node < n; ++node) {
    if (visited[node]) continue;
    dfs(node, visited, nodeStack);
}
  1. 此時nodeStack已經儲存了整個拓撲排序序列,我們只需要轉移到sort陣列並返回即可:
while (!nodeStack.empty()) {
    sort.push_back(nodeStack.top());
    nodeStack.pop();
}
return sort;

整個程式碼如下:

vector<node_t> Graph::toposortDfs() {
    vector<node_t> sort;
    vector<bool> visited(n, false);
    stack<node_t> nodeStack;
    for (node_t node = 0; node < n; ++node) {
        if (visited[node]) continue;
        dfs(node, visited, nodeStack);
    }
    while (!nodeStack.empty()) {
        sort.push_back(nodeStack.top());
        nodeStack.pop();
    }
    return sort;
}

測試

程式碼:

int main() {
    Graph graph{{2},
                {2},
                {3, 4, 5},
                {4},
                {6, 7},
                {4},
                {7},
                {}};
    auto sort = graph.toposortDfs();
    cout << "The topology sort sequence is:\n";
    for (const auto &node: sort) {
        cout << node << ' ';
    }
    return 0;
}

輸出:

The topology sort sequence is:
1 0 2 5 3 4 6 7

複雜度分析

  • 時間複雜度:\(O(n+e)\)\(n\)為節點總數,\(e\)為邊的總數。其中DFS的時間複雜度為\(O(n+e)\)
  • 空間複雜度:\(O(n)\)(鄰接表的空間複雜度為\(O(n+e)\),不計入在內),其中維護visited陣列和nodeStack棧分別需要\(O(n)\)的額外空間。

相關文章