1. 程式人生 > >08-圖9 關鍵活動   (30分)

08-圖9 關鍵活動   (30分)

假定一個工程專案由一組子任務構成,子任務之間有的可以並行執行,有的必須在完成了其它一些子任務後才能執行。“任務排程”包括一組子任務、以及每個子任務可以執行所依賴的子任務集。

比如完成一個專業的所有課程學習和畢業設計可以看成一個本科生要完成的一項工程,各門課程可以看成是子任務。有些課程可以同時開設,比如英語和C程式設計,它們沒有必須先修哪門的約束;有些課程則不可以同時開設,因為它們有先後的依賴關係,比如C程式設計和資料結構兩門課,必須先學習前者。

但是需要注意的是,對一組子任務,並不是任意的任務排程都是一個可行的方案。比如方案中存在“子任務A依賴於子任務B,子任務B依賴於子任務C,子任務C又依賴於子任務A”,那麼這三個任務哪個都不能先執行,這就是一個不可行的方案。

任務排程問題中,如果還給出了完成每個子任務需要的時間,則我們可以算出完成整個工程需要的最短時間。在這些子任務中,有些任務即使推遲幾天完成,也不會影響全域性的工期;但是有些任務必須準時完成,否則整個專案的工期就要因此延誤,這種任務就叫“關鍵活動”。

請編寫程式判定一個給定的工程專案的任務排程是否可行;如果該排程方案可行,則計算完成整個工程專案需要的最短時間,並輸出所有的關鍵活動。

輸入格式:

輸入第1行給出兩個正整數NNN(≤100\le 100100)和MMM,其中NNN是任務交接點(即銜接相互依賴的兩個子任務的節點,例如:若任務2要在任務1完成後才開始,則兩任務之間必有一個交接點)的數量。交接點按1~NN

N編號,MMM是子任務的數量,依次編號為1~MMM。隨後MMM行,每行給出了3個正整數,分別是該任務開始和完成涉及的交接點編號以及該任務所需的時間,整數間用空格分隔。

輸出格式:

如果任務排程不可行,則輸出0;否則第1行輸出完成整個工程專案需要的時間,第2行開始輸出所有關鍵活動,每個關鍵活動佔一行,按格式“V->W”輸出,其中V和W為該任務開始和完成涉及的交接點編號。關鍵活動輸出的順序規則是:任務開始的交接點編號小者優先,起點編號相同時,與輸入時任務的順序相反。

輸入樣例:

7 8
1 2 4
1 3 3
2 4 5
3 4 3
4 5 1
4 6 6
5 7 5
6 7 2

輸出樣例:

17
1->2
2->4
4->6
6->7

解析:先做一次拓撲排序(計算入度)算出每個活動的最早完成時間,參照How long does it take, 然後再反過來做一次拓撲排序(計算出度)算出每個活動的最晚完成時間,順便也算出所有活動的機動時間,最後機動時間等於0的就是關鍵路徑。另一種方法可以用堆疊來存第一次的拓撲序列,這樣就不需要算出度了。

#include <cstdio>
#include <cstdlib>
#include <queue>
using namespace std;
#define MAX 105
#define INFINITY 65535
int N, M, A[MAX][MAX], ECT, EarliestTime[MAX] = {0}, LatestTime[MAX], D[MAX][MAX], idx;  //ETC--earliest completion time
int getMax( int arr[] ) {
	int max = 0;
	for(int i = 0; i < N; i++)
		if( max < arr[i] ){
			max = arr[i];
			idx = i;
		}
	return max;
}
int TopSort_Earliest(){
	int V, cnt = 0, Indegree[MAX] = {0};
	queue<int> q;
	//計算各結點的入度
	for( int i = 0; i < N; i++ )
		for( int j = 0; j < N; j++ )
			if( A[i][j] != INFINITY )
				Indegree[j]++;	//對於有向邊<i,j>累計終點j的入度
	//入度為0的入隊
	for( int i = 0; i < N; i++ )
		if( Indegree[i] == 0 )
			q.push(i);
	while( !q.empty() ) {
		V = q.front();
		q.pop();
		cnt++;
		for( int j = 0; j < N; j++ )
			if( A[V][j] != INFINITY ) {  //<V, j>有有向邊
				if( EarliestTime[V] + A[V][j] > EarliestTime[j] ){ //如果 V的最早完成時間 + j所需時間 > j的最早完成時間
					EarliestTime[j] = EarliestTime[V] + A[V][j];
				}
				if( --Indegree[j] == 0 )  //去掉V後,如果j的入度為0
					q.push(j);
			}
	}
	ECT = getMax(EarliestTime); //最早完成時間應是所有元素中最大的
	if( cnt != N ) return 0;	//如果沒有取出所有元素,說明圖中有迴路
	else return 1;
}
void TopSort_Latest() {
	int V, Outdegree[MAX] = {0};
	queue<int> q;
		//計算各結點的出度
	for( int i = 0; i < N; i++ )
		for( int j = 0; j < N; j++ )
			if( A[i][j] != INFINITY )
				Outdegree[i]++;	//對於有向邊<i,j>累計起點i的出度
	//出度為0的入隊
	for( int i = 0; i < N; i++ )
		if( Outdegree[i] == 0 )
			q.push(i);
	//初始化LatestTime
	for( int i = 0; i < N; i++ )
		LatestTime[i] = INFINITY;
	LatestTime[idx] = ECT;	//將最後一個活動的最晚完成時間設為它的最早完成時間
	while( !q.empty() ) {
		V = q.front();
		q.pop();
		//cnt++;	//不需要再算cnt了
		for( int j = 0; j < N; j++ )
			if( A[j][V] != INFINITY ) {  //<j, V>有有向邊
				if( LatestTime[V] - A[j][V] <= LatestTime[j] ) {  //必須用<=,只<的話只能算一條關鍵路徑,<=才能算出所有的關鍵路徑(錯誤原因)
					LatestTime[j] = LatestTime[V] - A[j][V];
					D[j][V] = LatestTime[V] - EarliestTime[j] - A[j][V];
				}
				if( --Outdegree[j] == 0 )  //去掉V後,如果j的出度為0
					q.push(j);
			}
	}
}
void PrintKeyRoute() {
	for( int i = 0; i < N; i++ )
		for( int j = N - 1; j >= 0; j-- )	//根據題目要求,i相同時要j要逆序輸出
			if( D[i][j] == 0 )
				printf("%d->%d\n", i + 1, j + 1);
}
int main(){
	int a, b;
	scanf("%d %d", &N, &M);
	//初始化圖的邊A,各組的機動時間D
	for( int i = 0; i < N; i++ )
		for( int j = 0; j < N; j++ )
			D[i][j] = A[i][j] = INFINITY;
	//read
	for( int i = 0; i < M; i++ ) {
		scanf("%d %d", &a, &b);
		scanf("%d", &A[--a][--b]);	//題目中編號從1開始(錯誤原因)
	}
	if( !TopSort_Earliest() )
		printf("0\n");
	else {
		printf("%d\n", ECT);
		TopSort_Latest();
		PrintKeyRoute();
	}
	system("pause");
	return 0;
}