拓撲排序 (DFS和BFS及判斷是否有環)


一、什么是拓撲排序?

在圖論中,拓撲排序(Topological Sorting)是一個有向無環圖(DAG, Directed Acyclic Graph)的所有頂點的線性序列。且該序列必須滿足下面兩個條件:

  • 每個頂點出現且只出現一次。
  • 若存在一條從頂點 A 到頂點 B 的路徑,那么在序列中頂點 A 出現在頂點 B 的前面。

有向無環圖(DAG)才有拓撲排序,非DAG圖沒有拓撲排序一說。

拓撲排序其實還是挺奇妙的,就是解決誰先誰后的問題

例如,下面這個圖:

它是一個 DAG 圖,那么如何寫出它的拓撲排序呢?這里說一種比較常用的方法:
  1. 從 DAG 圖中選擇一個沒有前驅(即入度為0)的頂點並輸出。

  2. 從圖中刪除該頂點和所有以它為起點的有向邊。

  3. 重復 1 和 2 直到當前的 DAG 圖為空或當前圖中不存在無前驅的頂點為止

於是,得到拓撲排序后的結果是 { 1, 2, 4, 3, 5 }。

通常,一個有向無環圖可以有一個或多個拓撲排序序列。這是因為可能同時存在多個入度為0的結點,這時,先處理哪個都是可以的。

二. 拓撲排序用來干什么?

  • 判斷是否有環
  • 求DAG中的最長鏈

拓撲排序有兩種方式,就是bfs和dfs,一般書中介紹的大多數是bfs,大家就以為拓撲排序只有一種辦法,其實是不對的。

參考鏈接 :https://blog.csdn.net/weixin_43918531/article/details/86740991

三、拓撲排序的bfs模板

有一個明確的思路:每一個頂點都有入度和出度,入度為0說明沒有指向他的,那么就從他開始往下找。把這個入度為0的push進隊列(還要注意保存入度為0的點),同時把與這個點相連的所有點的入度-1,然后再看看有沒有入度為0的,有的話繼續push,循環上面的操作,直到沒有入度為0的點。
看一下上面的圖,如果從序號1開始的話:1入度為0,push進隊列,頂點2的入度-1,所以頂點2push進隊列,3和5的入度-1,3push進隊列,5push進隊列,頂點3進隊列后,頂點4入度-1,頂點4push進隊列,所以輸出結果就是1 2 3 5 4

#include <bits/stdc++.h>

using namespace std;
const int INF = 0x3f3f3f3f;


//本代碼功能:以bfs輸出一個有向無環圖DAG的拓撲序
const int N = 1010;
vector<int> edge[N];    //鄰接表
int ind[N];             //入度數組
queue<int> q;          //隊列

int n;          //n個結點
int m;          //m條邊
int main() {
    //讀入,建圖
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int x, y;
        cin >> x >> y;
        edge[x].push_back(y);
        ind[y]++;//維護入度
    }
    //入度為零的放入隊列
    for (int i = 1; i <= n; i++) if (!ind[i]) q.push(i);

    //廣度優先搜索DAG,就是拓撲排序的模板
    while (!q.empty()) {
        int x = q.front();
        q.pop();
        //輸出拓撲序
        cout << x << " ";
        for (int i = 0; i < edge[x].size(); i++) { //遍歷所有出邊
            int y = edge[x][i];                    //目標結點
            //對接點入度-1,抹去這條入邊
            ind[y]--;
            //如果入度為0,則入隊列,准備處理它
            if (!ind[y]) q.push(y);
        }
    }
    return 0;
}
/**
測試數據
 5 4

 1 2
 2 3
 2 5
 3 4

參考答案:
1 2 5 3 4
 */

四、拓撲排序的dfs模板

DFS是從一個點不斷往下遞歸,比如說從序號1往下遞歸,有箭頭就一直往下進行,直到到了最后一個元素,就開始往棧里(當然也可以是vector之類的,只不過需要反向再輸出)push元素。比如說上面的從序號1開始,到序號2,序號3,序號4,到盡頭了,就把4push進棧中,3push進棧,這個時候由於5也是2的下一個元素,所以5push進棧中,2push進棧,1push進棧,然后輸出就是1 2 5 3 4.
當然這個遞歸的順序是與你輸入的順序有關的,不過思路都是這樣的,由起始點向下遞歸。

#include <bits/stdc++.h>

using namespace std;
const int INF = 0x3f3f3f3f;

//本代碼功能:以dfs輸出一個有向無環圖DAG的拓撲序
const int N = 1010;

bool st[N];             //標識是不是已經使用過
vector<int> edge[N];    //鄰接表
vector<int> res;        //拓撲序列

/**
 * 功能:深度優先搜索,記錄拓撲序
 * @param u
 */
void dfs(int u) {
    //如果訪問過了,則返回,不再重復訪問
    if (st[u])return;
    //標識u結點已使用
    st[u] = true;

    //遍歷每個出邊,找到下一組結點
    for (int v:edge[u]) if (!st[v]) dfs(v);

    //這一層完畢才把它自己扔進去,最后扔等於最先輸出,因為后面是倒序輸出的
    res.push_back(u);
}

int n;          //n個結點
int m;          //m條邊
int main() {
    //讀入,建圖
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int x, y;
        cin >> x >> y;
        edge[x].push_back(y);
    }
    //將所有結點進行深入優先搜索
    for (int i = 1; i <= n; i++) dfs(i);

    //輸出,從后向前輸出
    for (int i = res.size() - 1; i >= 0; i--)
        cout << res[i] << " ";
}

/**
測試數據
 5 4

 1 2
 2 3
 2 5
 3 4

參考答案:
1 2 5 3 4
 */

五、dfs判斷是否有環

#include <bits/stdc++.h>
using namespace std;

/**
 5 5

 1 2
 2 3
 2 5
 3 4
 3 1

對比上個例子,添加了一條3->1的邊,就成了有向有環圖.
本題,也就不能輸出拓撲序了,因為有環圖沒有拓撲序,拓撲序是針對DAG的。可以判斷是否有環。
 */

//本代碼功能:以dfs判斷一個有向圖SDG是否有環
const int N = 1010;

int st[N];          //標識是不是已經使用過
vector<int> edge[N];//鄰接表
int n;              //n個結點
int m;              //m個關系

/**
 * 功能:深度優先搜索,判斷以u開頭的圖中是否有環,有環:true,無環:false
  有向有環圖dfs判斷是否有環只需要把st[]的狀態改一下,原本是兩種狀態,0和1,
  現在改成 0,1,-1
  0:代表未訪問
 -1:代表訪問完畢
  1:代表是這一階段正在訪問的(這一階段指的是兩個元素在同一個遞歸中)。
 */
bool dfs(int u) {
    //標識u結點正在訪問
    st[u] = 1;

    //遍歷每個出邊,找到下一組結點
    for (int v:edge[u]) {
        //如果遇到了正在訪問的結點,那么說明有環
        if (st[v] == 1) return true;
        //如果v這個結點沒有訪問過,遞歸查找v結點是否在環中
        if (st[v] == 0 && dfs(v)) return true;
    }

    //標識u結點訪問完畢
    st[u] = -1;
    return false;
}

int main() {
    //讀入,建圖
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int x, y;
        cin >> x >> y;
        edge[x].push_back(y);
    }
    //將所有未訪問過的結點進行深入優先搜索判斷是否有環
    for (int i = 1; i <= n; i++)
        if (!st[i] && dfs(i))//沒有訪問過,並且有環
            cout << "發現環!" << endl;
    return 0;
}

六、bfs判斷是否有環

#include <bits/stdc++.h>

using namespace std;

/**
 5 5

 1 2
 2 3
 2 5
 3 4
 3 1

對比上個例子,添加了一條3->1的邊,就成了有向有環圖.
本題,也就不能輸出拓撲序了,因為有環圖沒有拓撲序,拓撲序是針對DAG的。可以判斷是否有環。
 */

//本代碼功能:以bfs判斷一個有向圖SDG是否有環
const int N = 1010;

int st[N];          //標識是不是已經使用過
vector<int> edge[N];//鄰接表
int ind[N];         //入度表
int n;              //n個結點
int m;              //m個關系
queue<int> q;       //隊列
vector<int> ans;   //ans 為拓撲序列
int main() {
    //讀入,建圖
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int x, y;
        cin >> x >> y;
        edge[x].push_back(y);
        ind[y]++;
    }
    for (int i = 1; i <= n; i++) if (ind[i] == 0) q.push(i);  //將入度為0的點入隊列

    while (!q.empty()) {
        int p = q.front();
        q.pop(); // 選一個入度為0的點,出隊列
        ans.push_back(p);
        for (int i = 0; i < edge[p].size(); i++) {
            int y = edge[p][i];
            ind[y]--;
            if (ind[y] == 0) q.push(y);
        }
    }
    if (ans.size() == n) {
        for (int i = 0; i < ans.size(); i++)
            printf("%d ", ans[i]);
        printf("\n");
    } else printf("No Answer!\n");   //  ans 中的長度與n不相等,就說明無拓撲序列

    return 0;
}


免責聲明!

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



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