ACM周總結——拓撲排序

1、圖論——拓撲排序詳述

1.1引例

一個較大的工程往往被劃分成許多子工程,我們把這些子工程稱作活動。在整個工程中,有些子工程必須在其它有關子工程完成之後才能開始,有些子工程可以安排在任何時間開始。整個工程中各個子工程之間的先後關係可用一個有向圖來表示,圖中的頂點代表活動(子工程),圖中有向邊的起點的活動是終點活動的前序活動,只有當起點活動完成之後,其終點活動才能進行。這種頂點表示活動、邊表示活動間先後關係的有向圖就是頂點活動網,即AOV網。
一個AOV網應該是一個有向無環圖,即不應該帶有迴路,因爲若帶有迴路,則迴路上的所有活動都無法進行。如何用算法去判斷這樣的一項工程安排是否合理(即是否存在迴路),我們就可以採用拓撲排序進項判斷。

1.2定義

拓撲排序(topological-sort)是由某個集合上的一個偏序得到該集合上的一個全序的過程。在圖論中,對一個有向無環圖G進行拓撲排序,是將G中所有頂點排成一個線性序列,使得對於圖中任意一條有向邊<u,v>,u在線性序列中出現在v之前。這樣的線性序列就是拓撲序列。一個有向無環圖的拓撲序列可能並不唯一,滿足上述要求的任一序列都被稱爲該圖的拓撲序列。
在實際應用中,拓撲排序也可應用於無向圖中,常見的要求爲:將圖中度爲0或1的頂點及其臨邊刪去,重複執行直至沒有頂點可以刪除(見第一道題目)。

1.3算法描述

1)從AOV網中算則入度爲零的一個頂點並標記訪問。
2)刪除該點的所有臨邊。
3)繼續判斷圖中是否還有入度爲零的頂點,若有繼續執行1),否則結束。

1.4複雜度

時間複雜度:O(n+m)(n爲頂點數量,m爲邊的數量)
空間複雜度:O(n)

1.5算法實現

vector<int>edge[maxn];//鄰接表存圖
int indir[maxn];//頂點入度記錄數組
bool vis[maxn];//標記頂點是否訪問
int n, m;//頂點和邊的數量

void T_sort()
{
    int cnt = 0; //記錄入隊的定點的數量
    queue<int>que;
    for(int i = 0; i < n; i++) //將入度爲零的頂點入隊
    {
        if(indir[i] == 0)
        {
            que.push(i);
            vis[i] = true;
            cnt++;
        }
    }
    while(!que.empty())
    {
        int u = que.front();
        que.pop();
        for(auto v : edge[u]) //將隊首頂點的出邊刪去
        {
            indir[v]--;
            if(indir[v] == 0 && !vis[v]) //若有新的入度爲零的頂點則入隊
            {
                que.push(v);
                vis[v] = true;
                cnt++;
            }
        }
    }

    if(cnt < n)
        cout << "No" << endl; //若入隊頂點數小於n說明圖中有存在環
    else
        cout << "Yes" << endl;
}
 

2、題目詳解

2.1 hdu5438——Ponds詳解

2.1.1 題目描述

題目來源:2015 ACM/ICPC Asia Regional Changchun Online
題目等級:銅牌題

Sample Input

1
7 7
1 2 3 4 5 6 7
1 4
1 5
4 5
2 3
2 6
3 6
2 7
Sample Output

21

2.1.2 題目分析

題目大意是有n個池塘和m個管道;每個池塘的價值是v, 現在由於資金問題要刪除池塘;但是每次刪除的池塘最多隻連接一個管道,否則會爆炸
首先,根據題意要建立一個無向圖,每個池塘作爲一個節點,管道作爲邊,池塘的容量需要單獨記錄。題目中有一段表述爲“只能刪除管道數量小於2的池塘”和“不斷移除直至沒有滿足條件的池塘可以移除”,根據這兩個要求不難想到可以使用拓撲排序解決移除池塘的操作。
進行拓撲排序之後便可以進行第二步操作,找到圖中的連通塊,判斷每個連通塊的頂點數量,若頂點數量爲奇數則將連通塊內池塘的總容量累加。這一部分的操作可以通過BFS或DFS實現。題解中採用的是BFS。

2.1.3 解題思路

首先進行建圖,使用鄰接表存圖,另外定義兩個數組,一個用於記錄每個頂點的度,一個用於記錄每個頂點(池塘)的容量。根據題目給定的條件,數組的大小開到10010即可,容量、度數組類型使用int足夠。由於是多組樣例,建圖的時候需要注意初始化。
其次進行拓撲排序的操作。先定義一個標記數組,用於標記被刪除的頂點。之後遍歷度數組,將度爲0的節點標記刪除;將度爲1的數組入隊。之後循環進行拓撲排序的操作,直至沒有度爲1的頂點。
最後使用BFS查找圖中的連通塊。定義兩個變量,一個用於統計連通塊中的頂點個數,一個用於累加連通塊中的總容量。這個地方需要注意,累加容量的變量類型應該是long long類型,因爲多個池塘總容量會超過int的範圍。最後判斷每個連通塊的頂點個數,若爲奇數則累加總容量,否則不做任何處理。
最後即可得到正確答案。

2.1.4 題解代碼

#include <cstdio>
#include <vector>
#include <queue>
using namespace std;

const int maxn = 10010;

vector<int> edge[maxn];//鄰接表存圖
int val[maxn];//記錄每個池塘(節點)的容量
int deg[maxn];//記錄與每個池塘相連的池塘數量(節點的度)
int vis[maxn];//訪問標記
int n, m, T, u, v;

int main()
{
    scanf("%d", &T);
    while(T--)
    {
        long long sum = 0;
        scanf("%d%d", &n, &m);
        for(int i = 1; i <= n; i++) //初始化
        {
            scanf("%d", &val[i]);
            deg[i] = 0;
            edge[i].clear();
            vis[i] = 0;
        }

        for(int i = 0; i < m; i++) //建圖
        {
            scanf("%d%d", &u, &v);
            edge[u].push_back(v);
            edge[v].push_back(u);
            deg[u]++;
            deg[v]++;
        }

        queue<int>que; //拓撲排序
        for(int i = 1; i <= n; i++)
        {
            if(deg[i] == 0) //度爲零的池塘直接刪去
                vis[i] = 1;
            else if(deg[i] == 1) //度爲一的節點入隊準備拓撲排序
            {
                que.push(i);
                vis[i] = 1;
            }
        }
        while(que.size()) //拓撲排序
        {
            int t = que.front();
            que.pop();
            for(auto e : edge[t]) //將能到達的節點的度減一
            {
                if(!vis[e])
                {
                    deg[e]--;
                    if(deg[e] == 1) //將度爲一的節點入隊
                    {
                        que.push(e);
                        vis[e] = 1;
                    }
                }
            }
        }

        for(int i = 1; i <= n; i++) //BFS找連通塊
        {
            if(!vis[i])
            {
                queue<int>que;
                que.push(i);
                vis[i] = 1;
                int num = 0; //統計連通塊的節點數量
                long long cnt = 0; //統計連通塊的總容量
                while(que.size())
                {
                    int t = que.front();
                    que.pop();
                    cnt += val[t];
                    num++;
                    for(auto e : edge[t])
                        if(!vis[e])
                        {
                            que.push(e);
                            vis[e] = 1;
                        }
                }
                if(num % 2 == 1 && num > 2) //節點數爲奇數時累加
                    sum += cnt;
            }
        }

        printf("%lld\n", sum);
    }
    return 0;
}

2.1.5 小結

這道題目的關鍵在於刪除所有符合條件的頂點(即拓撲排序部分),需要注意的是,在刪除頂點的時候,度爲零的頂點也是符合條件的頂點。另外這道題還有一個小坑,就是在統計每個連通塊的總容量的時候(BFS過程中),需要使用long long類型的變量。由於這兩個點沒有注意到,在做題的時候WA了兩次。

3.1 hdu5638——Toposort詳解

3.1.1 題目描述

題目來源:BestCoder Round #74 (div.2)
題目等級:銅牌題

Sample Input

3
4 2 0
1 2
1 3
4 5 1
2 1
3 1
4 1
2 3
2 4
4 4 2
1 2
2 3
3 4
1 4
Sample Output

30
27
30

3.1.2 題目分析

題目大意是給出一個n個點m條邊的有向無環圖,要求刪去k條邊後使得該圖的拓撲序列的字典序儘可能小。最後輸出拓撲序列的每一位與對應的下標的乘積之和(取餘)。
根據題意容易想到使用優先隊列,使用貪心的思想每次儘可能的將較小的頂點刪去,這樣得出的拓撲序列節課達到最小字典序。

3.1.3 解題思路

定義一個優先隊列(小值優先),首先遍歷儲存頂點入度的數組,將所有入度不大於k的頂點入隊,之後進行拓撲排序,每次取出隊中編號最小的頂點並將其入度與當前的k值進行比較,若符合條件即可刪去。否則應當將其放回圖中(即將其訪問標記復原),這就是本題的關鍵所在。
最後按照題意求出累加和。

3.1.4 題解代碼

#include <cstdio>
#include <iostream>
#include <vector>
#include <queue>
using namespace std;

const int maxn = 1e5 + 10;
const int maxm = 2e5 + 10;
const int mod = 1e9 + 7;

vector<int>edge[maxn];//鄰接表存圖
int indir[maxn];//頂點入度記錄數組
bool vis[maxn];//標記頂點是否訪問
int T, n, m, k;

int main()
{
    int u, v;
    scanf("%d", &T);
    while(T--)
    {
        scanf("%d%d%d", &n, &m, &k);
        for(int i = 1; i <= n; i++)//初始化
        {
            edge[i].clear();
            indir[i] = vis[i] = 0;
        }
        for(int i = 0; i < m; i++)//建圖
        {
            scanf("%d%d", &u, &v);
            edge[u].push_back(v);
            indir[v]++;
        }

        long long sum = 0, cnt = 1;
        //使用優先隊列將值較小的頂點先出隊
        priority_queue<int, vector<int>, greater<int>>que;
        for(int i = 1; i <= n; i++)
        {
            if(indir[i] <= k)//關鍵點:將所有可能刪除的頂點全部入隊
            {
                que.push(i);
                vis[i] = true;
            }
        }
        while(!que.empty())
        {
            int u = que.top();
            que.pop();
            if(indir[u] > k)//關鍵點:若k值不足,則將其訪問標記復原
            {
                vis[u] = 0;
                continue;
            }

            k -= indir[u];
            sum = (sum + u * cnt++) % mod;
            for(auto v : edge[u]) //將刪去的頂點的出邊刪去
            {
                indir[v]--;
                if(indir[v] <= k && !vis[v]) //若有新的符合條件的頂點則入隊
                {
                    que.push(v);
                    vis[v] = true;
                }
            }
        }
        printf("%lld\n", sum);
    }

    return 0;
}

3.1.5 小結

這道題有兩個地方需要特別注意,一個是每次入隊的頂點應當是所有符合條件的頂點,即入度不大於當前k值的頂點,在出隊的時候還應當判斷一下其入度是否仍然不大於k,因爲在一個頂點入隊之後的過程中k值會發生變化,當k值不足以將其入邊刪去時,不能將該頂點刪去。
第二點是題目中要求必須刪掉k條邊,這個其實是一個坑點,實際寫代碼時不用考慮是否會刪夠k,只需考慮是否會多刪。因爲在刪除頂點的時候,所有的邊都可以算進k條邊中,故一定能刪夠k條邊。

4.1 hdu2647——Reward詳解

4.1.1 題目描述

題目來源:——
題目等級:銅牌題
在這裏插入圖片描述

Sample Input

2 1
1 2
2 2
1 2
2 1
Sample Output

1777
-1

4.1.2 題目分析

題目大意是分配獎金,共有n個人m個要求,每個要求(有向邊<u,v>)表示u的獎金額要大於v的獎金額。每個人獎金額最少爲888,問滿足要求的情況下所有人的獎金總額最少爲多少。若不能滿足所有要求輸出-1.
看到題目容易想到使用拓撲排序檢查圖中是否存在有向環,但這道題目關鍵點並不在於檢測有向環,而在於確定每個人的獎金額。如果按照普通的拓撲排序的話應當是從入度爲零的頂點開始的,但是我們無法確定最大的獎金額爲多少(如圖中的DEGH頂點),所以這道題應當採用反向建圖或者反向拓撲的方法。

4.1.3 解題思路

首先反向建圖,從獎金最少的人開始,記錄入度,之後先從入度爲零的開始拓撲,按照圖中的方式在隊列中入隊一個-1以標記每一層(即獎金額相同的人),這樣拓撲完畢後就能得到最少的總獎金額。

4.1.4 題解代碼

#include <cstdio>
#include <vector>
#include <queue>
using namespace std;

const int maxn = 1e5 + 10;
const int maxm = 2e5 + 10;
const int mod = 1e9 + 7;

vector<int>edge[maxn];//鄰接表存圖
int indir[maxn];//頂點入度記錄數組
bool vis[maxn];//標記頂點是否訪問
int T, n, m, k;

int main()
{
    int u, v;
    while(~scanf("%d%d", &n, &m))
    {
        for(int i = 1; i <= n; i++)//初始化
        {
            edge[i].clear();
            indir[i] = vis[i] = 0;
        }
        for(int i = 0; i < m; i++)//關鍵:反向建圖,從獎金最少的開始拓撲
        {
            scanf("%d%d", &u, &v);
            edge[v].push_back(u);
            indir[u]++;
        }

        //cnt記錄訪問的人數,sum記錄總獎金額,num標記當前獎金額
        int cnt = 0, sum = 0, num = 0;
        queue<int>que;
        for(int i = 1; i <= n; i++)
        {
            if(indir[i] == 0)
            {
                que.push(i);
                cnt++;
            }
        }
        que.push(-1);//關鍵:用於分隔獎金額相同的人
        while(!que.empty())
        {
            int u = que.front();
            que.pop();
            if(u == -1 && que.empty())
                break;
            if(u == -1)
            {
                que.push(-1);
                num++;
                continue;
            }
            sum += num;
            for(auto v : edge[u]) //將刪去的頂點的出邊刪去
            {
                indir[v]--;
                if(indir[v] == 0 && !vis[v])
                {
                    que.push(v);
                    vis[v] = true;
                    cnt++;
                }
            }
        }
        if(cnt < n)
            printf("-1\n");
        else
            printf("%d\n", sum + 888 * n); //人數足夠,爲每個人加上888
    }

    return 0;
}

3、圖論——拓撲排序的應用總結

拓撲排序原理比較簡單,運用起來代碼也不長,所以在正式的比賽當中很少會有僅考察拓撲排序的題目。實際上拓撲排序就是基於BFS的思想演變出來的算法,所以一旦出現拓撲排序,要麼會結合其他的算法或思維,如網絡流、最短路、DFS、貪心等,要麼就是考察的拓撲排序的思維和演變的算法,因此在拓撲排序這一塊,還需要多結合其它算法進行考慮,最重要的就是拓撲排序的方法,要多做拓撲排序的變種題,做到能夠熟練將拓撲排序進行各種方式的變換。
出題方面,也會結合其他的算法或者只是把拓撲排序當作題目中的一個步驟組合出將爲複雜的圖論題目,而不直接考察拓撲排序。出題的方向不侷限於圖論的題目,在字符串方向也可以結合拓撲排序出題。

4、參考資料列表

博客:
[1]字典序最小序列:https://blog.csdn.net/wodemale/article/details/89710463
[2]拓撲排序(DFS):https://www.cnblogs.com/dzkang2011/p/toplogicalSort_1.html
書籍:
算法導論 22.4節 拓撲排序

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