hihoCoder #1174:拓撲排序·一

描述

由於今天上課的老師講的特別無聊,小Hi和小Ho偷偷地聊了起來。

小Ho:小Hi,你這學期有選什麼課麼?

小Hi:挺多的,比如XXX1,XXX2還有XXX3。本來想選YYY2的,但是好像沒有先選過YYY1,不能選YYY2。

小Ho:先修課程真是個麻煩的東西呢。

小Hi:沒錯呢。好多課程都有先修課程,每次選課之前都得先查查有沒有先修。教務公佈的先修課程記錄都是好多年前的,不但有重複的信息,好像很多都不正確了。

小Ho:課程太多了,教務也沒法整理吧。他們也沒法一個一個確認有沒有寫錯。

小Hi:這不正是輪到小Ho你出馬的時候了麼!

小Ho:哎??

我們都知道大學的課程是可以自己選擇的,每一個學期可以自由選擇打算學習的課程。唯一限制我們選課是一些課程之間的順序關係:有的難度很大的課程可能會有一些前置課程的要求。比如課程A是課程B的前置課程,則要求先學習完A課程,纔可以選擇B課程。大學的教務收集了所有課程的順序關係,但由於系統故障,可能有一些信息出現了錯誤。現在小Ho把信息都告訴你,請你幫小Ho判斷一下這些信息是否有誤。錯誤的信息主要是指出現了”課程A是課程B的前置課程,同時課程B也是課程A的前置課程”這樣的情況。當然”課程A是課程B的前置課程,課程B是課程C的前置課程,課程C是課程A的前置課程”這類也是錯誤的。

提示:拓撲排序

小Ho拿出紙筆邊畫邊說道:如果把每一門課程看作一個點,那麼順序關係也就是一條有向邊了。錯誤的情況也就是出現了環。我知道了!這次我們要做的是判定一個有向圖是否有環。

小Hi:小Ho你有什麼想法麼?

<小Ho思考了一會兒>

小Ho:一個直觀的算法就是每次刪除一個入度爲0的點,直到沒有入度爲0的點爲止。如果這時還有點沒被刪除,這些沒被刪除的點至少組成一個環;反之如果所有點都被刪除了,則有向圖中一定沒有環。
[week47_2.png]

小Hi:Good Job!那趕快去寫代碼吧!

小Ho又思考了一會兒,撓了撓頭說:每次刪除一個點之後都要找出當前入度爲0的點,這一步我沒想到高效的方法。通過掃描一遍剩餘的邊可以找所有出當前入度爲0的點,但是每次刪除一個節點之後都掃描一遍的話複雜度很高。

小Hi讚許道:看來你已經養成寫代碼前分析複雜度的意識了!這裏確實需要一些實現技巧,才能把複雜度降爲O(N+M),其中N和M分別代表點數和邊數。我給你一個提示:如果我們能維護每個點的入度值,也就是在刪除點的同時更新受影響的點的入度值,那麼是不是就能快速找出入度爲0的點了呢?

小Ho:我明白了,這個問題可以這樣來解決:

  1. 計算每一個點的入度值deg[i],這一步需要掃描所有點和邊,複雜度O(N+M)。

  2. 把入度爲0的點加入隊列Q中,當然有可能存在多個入度爲0的點,同時它們之間也不會存在連接關係,所以按照任意順序加入Q都是可以的。

  3. 從Q中取出一個點p。對於每一個未刪除且與p相連的點q,deg[q] = deg[q] - 1;如果deg[q]==0,把q加入Q。

  4. 不斷重複第3步,直到Q爲空。

最後剩下的未被刪除的點,也就是組成環的點了。

小Hi:沒錯。這一過程就叫做拓撲排序。

小Ho:我懂了。我這就去實現它!

< 十分鐘之後 >

小Ho:小Hi,不好了,我的程序寫好之後編譯就出詭異錯誤了!

小Hi:詭異錯誤?讓我看看。

小Hi湊近電腦屏幕看了看小Ho的源代碼,只見小Ho寫了如下的代碼:

int edge[ MAXN ][ MAXN ];

小Hi:小Ho,你有理解這題的數據範圍麼?

小Ho:N最大等於10萬啊,怎麼了?

小Hi:你的數組有10萬乘上10萬,也就是100億了。算上一個int爲4個字節,這也得400億字節,將近40G了呢。

小Ho:啊?!那我應該怎麼?QAQ

小Hi:這裏就教你一個小技巧好了:

這道題目中N的數據範圍在10萬,若採用鄰接矩陣的方式來儲存數據顯然是會內存溢出。而且每次枚舉一個點時也可能會因爲枚舉過多無用的而導致超時。因此在這道題目中我們需要採用鄰接表的方式來儲存我們的數據:

常見的鄰接表大多是使用的指針來進行元素的串聯,其實我們可以通過數組來模擬這一過程。

int head[ MAXN + 1] = {0}; // 表示頭指針,初始化爲0
int p[ MAXM + 1]; // 表示指向的節點
int next[ MAXM + 1] = {0}; // 模擬指針,初始化爲0
int edgecnt; // 記錄邊的數量

void addedge(int u, int v) { // 添加邊(u,v)
++edgecnt;
p[ edgecnt ] = v;
next[ edgecnt ] = head[u];
head[u] = edgecnt;
}

// 枚舉邊的過程,u爲起始點
for (int i = head[u]; i; i = next[i]) {
v = p[i];

}

小Ho:原來還有這種辦法啊?好咧。我這就去改進我的算法=v=

輸入

第1行:1個整數T,表示數據的組數T(1 <= T <= 5)
接下來T組數據按照以下格式:
第1行:2個整數,N,M。N表示課程總數量,課程編號爲1..N。M表示順序關係的數量。1 <= N <= 100,000. 1 <= M <= 500,000
第2..M+1行:每行2個整數,A,B。表示課程A是課程B的前置課程。
輸出

第1..T行:每行1個字符串,若該組信息無誤,輸出"Correct",若該組信息有誤,輸出"Wrong"。

Sample Input

2
2 2
1 2
2 1
3 2
1 2
1 3

Sample Output

Wrong
Correct

代碼:

#include<stdio.h>
#include<string.h>
#include<string>
#include<queue>
#include<vector>
#include<algorithm>
using namespace std;
#define MAX 500005
int n,m,t,du[MAX];
queue<int> q;
vector<int> vec[MAX];
bool topsort()
{
    while(!q.empty())
        q.pop();//對每組測試數據清空隊列
    for(int i=1;i<=n;i++)
        if(du[i]==0)
            q.push(i);//將入度爲0的數放入隊列
    int sum=0;
    while(!q.empty())
    {
        int u=q.front();//讓u記錄目前隊列的第一個是哪個節點
        q.pop();//踢出隊列
        sum++;//記錄踢出了幾個節點
        for(int i=0;i<vec[u].size();i++)//循環次數爲u所對應的節點的個數
        {
            int temp=vec[u][i];//temp記錄u節點對應的那個節點,即main函數中存入vec的b
            if(--du[temp]==0)//判斷b的入度減一是不是零
                q.push(temp);//是0則放入隊列
        }
    }
        if(sum<n)//踢出的節點數是否小於總節點數,即判斷隊列裏剩沒剩節點
            return false;
        else
            return true;

}
int main()
{
    int a,b;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d",&n,&m);
        memset(du,0,sizeof(du));
        for(int i=1;i<=n;i++)
            if(vec[i].size())
                vec[i].clear();
        while(m--)
        {
            scanf("%d%d",&a,&b);
            vec[a].push_back(b);//意思相當於vec[a][0]=b(a=1,b=2時,vec[1][0]=2;輸入第二組a=1,b=3時,vec[1][1]=3)
            du[b]++;//讓a後面的b入度加一
        }
        if(topsort())
            printf("Correct\n");
        else
            printf("Wrong\n");
    }
return 0;
}

我還是程度太差了=_=,題解都看了好久好久才能強行理解。

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