描述
由於今天上課的老師講的特別無聊,小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:我明白了,這個問題可以這樣來解決:
計算每一個點的入度值deg[i],這一步需要掃描所有點和邊,複雜度O(N+M)。
把入度爲0的點加入隊列Q中,當然有可能存在多個入度爲0的點,同時它們之間也不會存在連接關係,所以按照任意順序加入Q都是可以的。
從Q中取出一個點p。對於每一個未刪除且與p相連的點q,deg[q] = deg[q] - 1;如果deg[q]==0,把q加入Q。
不斷重複第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;
}
我還是程度太差了=_=,題解都看了好久好久才能強行理解。