拓撲排序

拓撲排序

1. 算法分析

1.1 特色分析

    拓撲排序能夠在線性的時間複雜度 O(n + m) 內完成求出拓撲序的操做,對象是有向無環圖。
拓撲圖的性質以下:c++

  1. 有向圖纔有拓撲序
  2. 有向無環圖一定存在拓撲序
  3. 存在拓撲序 <=> 無環
  4. 有向無環圖至少存在一個入度爲0的點
  5. 當前的點隻影響後面的狀態,因此能夠dp處理

1.2 使用場景

    拓撲排序,能夠支持如下操做:算法

  1. 求出拓撲序:
    1.1 求通常拓撲序:若是是通常隊列,那麼求出的爲通常的拓撲序
    1.2 求字典序最大/最小拓撲序:若是是優先隊列,那麼求出的是字典序最大/最小拓撲序
  2. 拓撲序判斷環
    判斷圖中是否有環:若是原來的點數==最後拓撲序內的點數,那麼拓撲序惟一,無環;不然,有環
  3. 拓撲序+dp:
    3.1 求最短\長路:若是邊權所有大於0,那麼可使用拓撲排序找最短路
    3.2 求每一個點的可達性

2. 例題

2.1 求出拓撲序

2.1.1 通常拓撲序

#include <bits/stdc++.h>

using namespace std;

int const N = 1e5 + 10;
int e[N], ne[N], h[N], idx, d[N];
int n, m;
vector<int> ans;

// 創建鄰接表
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// 拓撲排序
void top_sort()
{
    queue<int> q;  // 維護一個隊列
    for (int i = 1; i <= n; ++i) if (!d[i]) q.push(i);  // 把入度爲0的點加入隊列
    // 當隊列不爲空時
    while (q.size())
    {
        auto t = q.front();  // 取隊頭
        q.pop();  // 隊頭出隊
        ans.push_back(t);  // 把這個數字放入答案序列
        for (int i = h[t]; i != -1; i = ne[i])  // 枚舉全部隊頭元素相鄰的元素
        {
            int j = e[i];
            d[j]--;  // 隊頭元素出隊至關於把與隊頭元素相連的元素的入度減一
            if (!d[j]) q.push(j);  // 把入度爲0的元素放入隊列
        }
    }
    if (ans.size() == n)   // 輸出答案序列
    {
        for (auto a: ans) printf("%d ", a);
    }
    else cout << "-1";
}

int main()
{
    cin >> n >> m;  // 輸入點數和邊數
    memset(h, -1, sizeof h);  // 初始化h
    for (int i = 0; i < m; ++i)  // 讀入每條邊
    {
        int a, b;
        scanf("%d %d", &a, &b);
        add(a, b);  // 把b插入a的邊表
        d[b]++;  // b的入度加一
    }
    top_sort();
    return 0;
}

2.1.2 求出字典序最大/最小的拓撲序

#include <bits/stdc++.h>

using namespace std;

int const N = 1e5 + 10;
int e[N], ne[N], h[N], idx, d[N];
int n, m;
vector<int> ans;

// 創建鄰接表
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// 拓撲排序
void top_sort()
{
    priority_queue<int, vector<int>, greater<int>> q;  // 這裏是求字典序最小的拓撲序,若是求字典序最大的,那麼改爲 priority_queue<int, vector<int>, less<int>> q;
    for (int i = 1; i <= n; ++i) if (!d[i]) q.push(i);  // 把入度爲0的點加入隊列
    // 當隊列不爲空時
    while (q.size())
    {
        auto t = q.top();  // 取隊頭
        q.pop();  // 隊頭出隊
        ans.push_back(t);  // 把這個數字放入答案序列
        for (int i = h[t]; i != -1; i = ne[i])  // 枚舉全部隊頭元素相鄰的元素
        {
            int j = e[i];
            d[j]--;  // 隊頭元素出隊至關於把與隊頭元素相連的元素的入度減一
            if (!d[j]) q.push(j);  // 把入度爲0的元素放入隊列
        }
    }
    for (int i = 0; i < ans.size(); ++i) {
        cout << ans[i];
        if (i != ans.size() - 1) cout << " ";
    }
    cout << endl;
}

int main()
{
    while (scanf("%d%d", &n, &m) != EOF) {
        memset(d, 0, sizeof d);
        ans.clear();
        idx= 0;
        memset(h, -1, sizeof h);  // 初始化h
        for (int i = 0; i < m; ++i)  // 讀入每條邊
        {
            int a, b;
            scanf("%d %d", &a, &b);
            add(a, b);  // 把b插入a的邊表
            d[b]++;  // b的入度加一
        }
        top_sort();
    }
    return 0;
}

HDU2857 逃生
題意: t個測試樣例,每一個測試樣例給出n和m,表示n個點、m條邊。下面m行給出m條有向邊信息a b,表示a->b。求出拓撲序,要求當拓撲序不惟一時,使得序號小的在前。
題解: 讓編號小的儘可能靠前,這個意思不是字典序最小的拓撲序。解法:反向建圖,優先隊列(大頂堆)求字典序最大的序列,倒着輸出即爲本題答案。理解:看上圖,咱們從1開始走,鄰接點3和4,咱們不知道後面還有個2,因此不知道3和4先選誰,故正向尋找是錯的。既然要求編號小的儘可能靠前,那咱們能夠考慮把編號大的放到後面去。
咱們反向走,從最後面往前走,優先走編號大的,也就是字典序最大。最後把序列倒着輸出,如此,就知足了本題。微信

#include <bits/stdc++.h>

using namespace std;

int const N = 1e5 + 10;
int e[N], ne[N], h[N], idx, d[N];
int n, m, t;
vector<int> ans;

// 創建鄰接表
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// 拓撲排序
void top_sort()
{
    priority_queue<int, vector<int>, less<int>> q;  // 維護一個從大到小的優先隊列
    for (int i = 1; i <= n; ++i) if (!d[i]) q.push(i);  // 把入度爲0的點加入隊列
    // 當隊列不爲空時
    while (q.size())
    {
        auto t = q.top();  // 取隊頭
        q.pop();  // 隊頭出隊
        ans.push_back(t);  // 把這個數字放入答案序列
        for (int i = h[t]; i != -1; i = ne[i])  // 枚舉全部隊頭元素相鄰的元素
        {
            int j = e[i];
            d[j]--;  // 隊頭元素出隊至關於把與隊頭元素相連的元素的入度減一
            if (!d[j]) q.push(j);  // 把入度爲0的元素放入隊列
        }
    }
    reverse(ans.begin(), ans.end());  // 反轉輸出
    for (int i = 0; i < ans.size(); ++i) {
        cout << ans[i];
        if (i != ans.size() - 1) cout << " ";
    }
    cout << endl;
}

int main()
{
    cin >> t;
    while (t--) {
        scanf("%d%d", &n, &m);
        memset(d, 0, sizeof d);
        ans.clear();
        idx= 0;
        memset(h, -1, sizeof h);  // 初始化h
        for (int i = 0; i < m; ++i)  // 讀入每條邊
        {
            int a, b;
            scanf("%d %d", &a, &b);
            add(b, a);  // 把b插入a的邊表
            d[a]++;  // b的入度加一
        }
        top_sort();
    }
    return 0;
}

2.2 判斷圖中是否有環

Codeforces Round #656 (Div. 3) E. Directing Edges
題意: 給定t個測試樣例,每一個測試樣例給定n和m,n爲圖的點數,m爲圖的邊數,給定m條邊,每條邊爲op, a, b。若是op == 1,表示有一條有向邊a -> b;若是op == 0,表示a和b之間有一條無向邊。如今要求把無向邊變成有向邊(把a--b變成a->b或b->a),使得最後這m條邊沒有環。\(\sum_{i=1}^n n、m\) ~ 2e5
題解: 題意能夠轉換爲如今有一張有向圖,要求向這張有向圖內加入有向邊,使得這張有向圖沒有環,求加邊的方法。所以,能夠先作一次拓撲排序,求出拓撲序,若是沒有拓撲序,說明已經存在環;不然,只須要加入拓撲序小的指向拓撲序大的邊便可。(由於想要造成環,必須有迴路,則必須a->b,同時b->a,一旦出現拓撲序,說明存在a->b,不存在b->a,所以只要不加入b->a,則不可能出現環)
代碼以下:less

#include<bits/stdc++.h>

using namespace std;

int const N = 2e5 + 10;
int t, n, m;
set<int> point;
int e[N * 2], ne[N * 2], idx, h[N];
int d[N], sorted[N];
vector<pair<int, int> > undirect, direct;
vector<int> ans;
struct Edge{
    int op, u, v;
};
vector<Edge> E;

void top_sort() {
    queue<int> q;
    for (int i = 1; i <= n; ++i) {
        if (!d[i]) {
            q.push(i);
        }
    }
    while (q.size()) {
        auto t = q.front();
        q.pop();
        ans.push_back(t);

        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            d[j]--;
            if (!d[j]) {
                q.push(j);
            }
        }
    }

    // 記錄一下拓撲序內的點的順序
    for (int i = 0; i < ans.size(); ++i) {
        sorted[ans[i]] = i + 1;
    }
    return ;
}

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
 
int main() {
    cin >> t;
    while (t--) {
        memset(h, -1, sizeof h);
        memset(d, 0, sizeof d);
        idx = 0;
        E.clear();
        memset(sorted, 0, sizeof sorted);
        ans.clear();
        cin >> n >> m;
        for (int i = 0, op, a, b; i < m; ++i) {
            scanf("%d%d%d", &op, &a, &b);
            E.push_back({op, a, b});
            if (op == 1) {
                add(a, b);
                d[b]++;
            }
        }
        top_sort();
        if (ans.size() != n) {
            cout << "NO\n";
            continue;
        }
        cout << "YES\n";
        for (int i = 0; i < E.size(); ++i) {
            if (E[i].op == 1) {
                cout << E[i].u << " " << E[i].v << endl;
            }
            else {
                // 按照拓撲序內順序小的指向順序大的
                if (sorted[E[i].u] < sorted[E[i].v]) cout << E[i].u << " " << E[i].v << endl;
                else cout << E[i].v << " " << E[i].u << endl;
            }
        }
    }
    return 0;
}

2.3 拓撲排序+dp

2.3.1 求最短路\最長路

acwing1192獎金
公司按照每一個人的貢獻給每一個人發獎金。獎金存在M對關係,每對關係爲a,b,表示a的獎金比b高。每位員工工資最少爲100元,問最少須要發多少獎金。測試

/*
本題是差分約束的簡化版,造成的邊只有正權邊
若是存在正環那麼無解,換言之,若是不存在拓撲序則無解,所以可使用拓撲排序來判斷
若是有解,求出拓撲序後,直接按照拓撲序更新最長路便可
*/
#include<bits/stdc++.h>

using namespace std;

int const N = 1e4 + 10, M = 2e4 + 10;
int n, m;
int din[N], dis[N];
int e[M], ne[M], h[N], idx;
vector<int> ans;

// 拓撲排序
bool topsort()
{
    queue<int> q;
    for (int i = 1; i <= n; ++i)
        if (!din[i]) q.push(i);
    
    while (q.size())
    {
        auto t = q.front();
        q.pop();
        ans.push_back(t);

        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            din[j]--;
            if (!din[j]) q.push(j);
        }
    }v

    return ans.size() == n;    
}

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

int main()
{
    // 建圖
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for (int i = 0; i < m; ++i)
    {
        int a, b;
        scanf("%d %d", &a, &b);
        add(b, a);
        din[a] ++;
    }

    // 拓撲排序判斷是否有解
    if (!topsort()) 
    {
        printf("Poor Xed\n");
        return 0;
    }

    // 按照拓撲排序更新最長路
    for (int i = 1; i <= n; ++i) dis[i] = 100;
    for (int i = 0; i < n; ++i)
    {
        int t = ans[i];
        for (int j = h[t]; ~j; j = ne[j])
        {
            int k = e[j];
            dis[k] = max(dis[k], dis[t] + 1);
        }
    }

    // 計算答案
    int ans = 0;
    for (int i = 1; i <= n; ++i) ans += dis[i];
    cout << ans << endl;
    return 0;
}

acwing456車站分級
題意: 一條單向的鐵路線上,依次有編號爲1, 2, …, n 的n個火車站。每一個火車站都有一個級別,最低爲1級。現有若干趟車次在這條線路上行駛,每一趟都知足以下要求:若是這趟車次停靠了火車站x,則始發站、終點站之間全部級別大於等於火車站x的都必須停靠。(注意:起始站和終點站天然也算做事先已知須要停靠的站點)
例如,下表是5趟車次的運行狀況。
其中,前4趟車次均知足要求,而第5趟車次因爲停靠了3號火車站(2級)卻未停靠途經的6號火車站(亦爲2級)而不知足要求。
微信截圖_20200723220613.png
現有m趟車次的運行狀況(所有知足要求),試推算這n個火車站至少分爲幾個不一樣的級別。1≤n,m≤1000
題解: 很明顯,不停靠的站點的優先級必定比停靠的站點的優先級要小,所以不停靠的站點的優先級最小爲1,且停靠的站點的優先級>=不停靠的站點的優先級+1,則本題能夠轉換爲一個差分約束問題,且邊權大於等於0。(這裏不須要tarjan判斷是否有正環,由於明確了有解,不可能出現正環,因此直接拓撲排序求拓撲序(tarjan的目的也是縮點完求拓撲序))。本題的另外一個難點在於建圖,若是直接把不停靠的站點向停靠的站點連一條邊,那麼建圖的複雜度爲O(mn^2^)。對於一個二分圖,左邊的每一個點都須要向右邊每一個點連一條邊的建圖模型來講,能夠設置一個虛擬節點,而後使得左邊每一個點連向虛擬節點,虛擬節點再向右邊每一個點連邊。這樣就把O(n ^ 2)優化到O(n)。
文檔 07-23.png優化

/*
本題是一道差分約束問題,對於每一輛車停靠的站點的優先級必定比沒停靠的站點的優先級要高,所以
全部未停靠的站點均可以連一條邊到停靠的站點。
可是若是這樣建圖將會建邊平方條,1e8,超時
考慮簡單的建邊方法:對於每輛車創建一個虛擬節點,這個虛擬節點能夠認爲是未停靠的車站的最高優先級,
那麼對於每一個關係b>=a+1則能夠從原來的a->b建權值爲1的邊變化爲a向虛擬節點ver建一條邊權爲0的邊,再從ver到b建一條
邊權爲1的邊,這樣子就能夠把平方的邊數降到線性,每輛車最多創建2000條邊,總共2e6條邊
建圖後,因爲本題特色保證了必定是一張DAG,因此要求最長路不須要判正環,直接拓撲排序後獲得順序,而後dp求最長路便可
*/
#include<bits/stdc++.h>

using namespace std;

int const N = 2e3 + 10, M = 2e6 + 10;
int n, m;
int e[M], ne[M], h[N], w[M], idx;
int din[N], st[N], dis[N];
vector<int> ans;

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

// 拓撲排序
void topsort()
{
    queue<int> q;
    for (int i = 1; i <= n + m; ++i) if (!din[i]) q.push(i);

    while (q.size())
    {
        auto t = q.front();
        q.pop();
        ans.push_back(t);

        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            din[j]--;
            if (!din[j]) q.push(j);
        }
    }
}

int main()
{
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for (int i = 1; i <= m; ++i)
    {
        // 建圖(加入虛節點,平方->線性)
        memset(st, 0, sizeof st);
        int cnt, start = n, end = 1, ver = n + i;  // start記錄最小的點,end記錄最大的點,ver記錄虛擬節點
        cin >> cnt;
        for (int i = 0; i < cnt; ++i)
        {
            int t;
            cin >> t;
            st[t] = 1;
            start = min(start, t);
            end = max(end, t);
        }

        // 把a->b的邊拆成a->ver和ver->b
        for (int j = start; j <= end; ++j)
        {
            if (st[j]) add(ver, j, 1), din[j]++;
            else add(j, ver, 0), din[ver]++;
        }
    }

    // 拓撲排序獲得更新的順序
    topsort();

    // dp求最大值
    for (int i = 1; i <= n; ++i) dis[i] = 1;
    for (int i = 0; i < ans.size(); ++i)
    {
        int k = ans[i];
        for (int j = h[k]; ~j; j = ne[j])
        {
            int t = e[j];
            dis[t] = max(dis[t], dis[k] + w[j]);
        }
    }

    int res = 0;
    for (int i = 1; i <= n; ++i) res = max(res, dis[i]);
    cout << res << endl;
    return 0;
}

2.3.2 求可達性

acwing164可達性統計
給定一張N個點M條邊的有向無環圖,分別統計從每一個點出發可以到達的點的數量。第一行兩個整數N,M,接下來M行每行兩個整數x,y,表示從x到y的一條有向邊。輸出共N行,表示每一個點可以到達的點的數量。1≤N,M≤30000spa

/*
由於是有向無環圖,因此當前狀態隻影響後面的狀態,因此可使用dp的思想處理
所以,先作一次拓撲排序,獲得拓撲序,然後使用逆拓撲序倒着向前更新每一個點的可達性
具體更新的時候可使用一個bitset便可維護當前每一個點的可達狀態
*/
#include <bits/stdc++.h>

using namespace std;

int const N = 3e4 + 10;
int e[N], ne[N], idx, h[N];
int n, m, d[N];
bitset<N> f[N];
vector<int> ans;

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void top_sort() {
    queue<int> q;
    for (int i = 1; i <= n; ++i) if (!d[i]) q.push(i);
    while (!q.empty()) {
        auto t = q.front();
        q.pop();
        ans.push_back(t);
        
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            d[j]--;
            if (!d[j]) q.push(j);
        }
    }
    return ;
}

int main(){
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for (int i = 1, a, b; i <= m; ++i) {
        scanf("%d%d", &a, &b);
        d[b] ++;
        add(a, b);
    }
    
    // 求出拓撲序
    top_sort();
    
    // 倒着更新求出可達性
    for (int i = ans.size() - 1; i >= 0; --i) {
        int j = ans[i];  // 當前這個點
        f[j][j] = 1;  // 當前這個點到本身是可達的
        for (int k = h[j]; ~k; k = ne[k]) {
            int t = e[k];  // j可以遍歷到的點是t
            f[j] |= f[t];  // j的狀態受t影響
        }
    }
    
    for (int i = 1; i <= n; ++i) cout << f[i].count() << endl;
    return 0;
}
相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息