資料結構課程設計報告
項 目 名 稱: 暢通工程
所 在 班 級:
小 組 成 員:
任 課 教 師:
起 止 時 間:
專案基本資訊
專案名稱 |
暢通工程 |
專案簡介 |
某地區經過對城鎮交通狀況的調查,得到現有城鎮間快速道路的統計資料,並提出“暢通工程”的目標:使整個地區任何兩個城鎮間都可以實現快速交通(但不一定有直接的快速道路相連,只要互相間接通過快速路可達即可)。由於實現暢通目標時要考慮的側重點不同,使得相應的解決方法也不相同。 解決各種問題的共同點是:我們總可以把各城鎮看成圖中的結點,連線兩城鎮的快速路看成邊,於是暢通工程問題就轉化為各種版本的圖論問題。 |
小組成員 |
|
任務分工 |
建設道路數量問題: 最低成本建設問題: |
專案評定成績記錄
指導教師意見 |
系統完成情況:優 良 中 差 |
|||
報告完成情況:優 良 中 差 |
||||
答辯評定成績 |
團隊整體成績: |
|||
成員成績 |
|
|
|
|
|
|
|
||
|
|
|
||
|
|
|
||
綜 合 成 績 |
|
問題描述及分析
寫清楚你要實現的是個什麼系統,完成的都是什麼功能?
1、建設道路數量問題
[問題描述] 現有城鎮道路的統計資料表中列出了每條快速路直接連通的城鎮,問最少還
需要建設多少條快速路就可以達到暢通目標?
輸人資料包括城鎮數目N和快速路數目M;隨後的M行對應M條快速路,每行給出一對正
整數,分別是該條快速路直接連通的兩個城鎮的編號。為簡單起見,城鎮從1到N編號。
要求輸出需要建設的快速路的條數。
[實現方法]我們把已經連通的一片城鎮區域看成圖的一個連通集,這個問題就等價於問
目前給定的圖中有多少個獨立的連通集,而連通K個集合,最少需要K-1條邊。
利用並查集,將有邊相連的結點都並人同一集合,最後數一下有多少個集合即可。
2、最低成本建設問題
[問題描述]現有城鎮道路的統計資料表中列出了有可能建設成快速路的若干條道路的成
本,求暢通工程需要的最低成本。
輸人資料包括城鎮數目N和候選道路數目M;隨後的M行對應M條道路,每行給出3個正.
整數,分別是該條道路直接連通的兩個城鎮的編號以及該道路改建的預算成本。為簡單起見,城
鎮從1到N編號。要求輸出暢通工程需要的最低成本。如果輸人資料不足以保證暢通,則輸出
“需要建設更多道路”。
[實現方法]我們把道路建設成本看成圖中對應邊的權重。要保證圖中N個結點的連通,
我們至少需要構建N-1條邊,使得結點連線成- -棵樹;而要求成本最低.就意味著N-1條邊的
總權重最小。這個問題就等價於求給定帶權圖的最小生成樹問題。
求最小生成樹的演算法,在此我們選擇Kruskal 演算法。
功能模組及資料結構描述
系統劃分成幾個模組,幾個模組之間是什麼呼叫關係?畫出系統結構圖
1、建設道路數量問題
系統劃分6個模組
圖1 建設道路數量問題模組劃分
2、最低成本建設問題
系統劃分12個模組
其中,CreateGraph和InsertEdge分別在main和Kruskal中呼叫一次
圖2 最低成本建設問題模組劃分
二、 主要演算法流程描述及部分核心演算法
主要模組的演算法介紹,畫出其流程圖
1、建設道路數量問題
檔名稱:
專案名稱:暢通工程問題
建立者:
建立時間:
最後修改時間:
功能: 計算出需要多少條快速路
檔案中的函式名稱和簡單功能描述:
檔案中定義的全域性變數和簡單功能描述:
檔案中用到的他處定義的全域性變數及其出處:
與其他檔案的依賴關係:
演算法介紹:
/*道路數量建設*/
#include<stdio.h>
#include<stdlib.h>
#define MAXN 1000
#define MaxVertexNum 100
typedef int ElementType;
typedef int SetName;
typedef ElementType SetType[MAXN];
typedef int Vertex;
void Union(SetType S,SetName Root1,SetName Root2); //合併集合 保證小集合併入大集合
SetName Find(SetType S,ElementType X);
void Initialization(SetType S,int N); //集合初始化預設從1開始
void InputConnection(SetType S,int M); //讀M條入邊,並將所有邊相連的節點併入統一集合
int CountConnectedComponents(SetType S,int N); //計算集合S中連通集的個數
int main(void)
{
SetType S;
int N,M;
while(scanf("%d",&N)!=EOF&&N!=0){
scanf("%d",&M);
Initialization(S,N);
InputConnection(S,M);
printf("需要建設道路%d條\n",CountConnectedComponents(S,N)-1);
/* 測試程式碼 */
// for(int i=1;i<=N;i++)
// printf("S[%d]=%d\n",i,S[i]);
}
return 0;
}
void Union(SetType S,SetName Root1,SetName Root2) //合併集合 保證小集合併入大集合
{
if(S[Root2]<S[Root1]) //如果集合2集合1大
{
S[Root2]+=S[Root1]; //集合1併入集合2
S[Root1]=Root2;
}
else //如果集合1比較大
{
S[Root1]+=S[Root2]; //集合2併入集合1
S[Root2]=Root1;
}
}
SetName Find(SetType S,ElementType X)
{
for(;S[X]>=0;X=S[X]);
return X;
}
void Initialization(SetType S,int N) //集合初始化預設從1開始
{
int i;
for(i=1;i<=N;i++)
S[i]=-1;
}
void InputConnection(SetType S,int M) //讀M條入邊,並將所有邊相連的節點併入統一集合
{
Vertex U,V;
Vertex Root1,Root2;
int i;
for(i=0;i<M;i++)
{
scanf("%d%d",&U,&V);
Root1=Find(S,U);
Root2=Find(S,V);
if(Root1!=Root2)
Union(S,Root1,Root2);
}
}
int CountConnectedComponents(SetType S,int N) //計算集合S中連通集的個數
{
int i;
int cnt=0;
for(i=1;i<=N;i++)
{
if(S[i]<0)
cnt++;
}
return cnt;
}
流程圖:
流程圖1 建設道路數量問題主函式
2、最低成本建設問題
檔名稱:
專案名稱:暢通工程問題
建立者:
建立時間:
最後修改時間:
功能:
檔案中的函式名稱和簡單功能描述:
檔案中定義的全域性變數和簡單功能描述:
檔案中用到的他處定義的全域性變數及其出處:
與其他檔案的依賴關係:
演算法介紹:
/*最低成本建設問題*/
#include <stdlib.h>
#include <stdio.h>
typedef int WeightType;// 邊的權值設為整型
typedef int ElementType;
typedef int SetName;
typedef int Vertex;
typedef int* SetType;
/* 鄰接點的定義 */
typedef struct AdjVNode *PtrToAdjVNode; //網圖的邊表結構
struct AdjVNode
{
Vertex AdjV;
WeightType Weight;
PtrToAdjVNode Next;
};
/* 頂點表頭結點的定義 */
typedef struct Vnode //頂點結構
{
PtrToAdjVNode FirstEdge;
}AdjList;
/* 圖節點的定義 */
typedef struct GNode *ptrToGNode;
struct GNode
{
int Nv;
int Ne;
AdjList *G;
};
typedef ptrToGNode LGraph;
/* 邊的定義 */
struct ENode
{
Vertex V1;
Vertex V2;
WeightType Weight;
};
typedef ENode* Edge;
/****************** 並查集 *********************/
void InitializeVSet(SetType S,int N); //初始化並查集
void Union(SetType S,SetName Root1,SetName Root2); //合併集合 保證小集合併入大集合
SetName Find(SetType S,ElementType X); //路徑壓縮
//檢查連線V1和V2的邊是否在現有的最小生成樹子集中構成迴路
bool CheckCycle(SetType VSet,Vertex V1,Vertex V2);
/***************** 最小堆 *********************/
//將N個元素的邊陣列中以ESet[p]為根的子堆調整為關於Weight的最小堆
void PercDown(Edge ESet,int p,int N);
void InitializeESet(LGraph Graph,Edge ESet); //將圖的邊存入陣列ESet,並且初始化為最小堆
void Swap(Edge a,Edge b);
int GetEdge(Edge ESet,int CurrentSize); //給定當前堆的大小CurrentSize,將當前最小邊位置彈出並調整堆
/************* 鄰接表的圖定義 ***************/
LGraph CreateGraph(Vertex MaxNumber);
void InsertEdge(LGraph MST,Edge edge);
/************* 核心 Kruskal演算法 **************/
int Kruskal(LGraph Graph,LGraph MST); //將最小生成樹儲存為鄰接表儲存的圖MST,返回最小權重和
int main(void)
{
Vertex Nv,Ne;
Vertex V1,V2,i;
WeightType Weight;
scanf("%d %d",&Nv,&Ne);
LGraph graph=CreateGraph(Nv);
LGraph MST;
for(i=0;i<Ne;i++)
{
Edge edge=(Edge)malloc(sizeof(ENode));
scanf("%d %d %d",&V1,&V2,&Weight);
V1--;
V2--;
edge->V1=V1;
edge->V2=V2;
edge->Weight=Weight;
InsertEdge(graph,edge);
++graph->Ne;
}
int total;
total=Kruskal(graph,MST);
if(total<0){
printf("\n需要建設更多道路\n");
} else{
printf("\n最低成本 = %d\n",total);
}
return 0;
}
/*-------------------- 並查集--------------------*/
void InitializeVSet(SetType S,int N) //初始化並查集
{
// S=(SetType)malloc(sizeof(int)*N);
ElementType X;
for(X=0;X<N;X++)
S[X]=-1;
}
void Union(SetType S,SetName Root1,SetName Root2) //合併集合 保證小集合併入大集合
{ /* 這裡預設Root1和Root2是不同集合的根結點 ,要保證小集合併入大集合 */
if(S[Root2]<S[Root1]) /* 如果集合2比較大 */
{
S[Root2]+=S[Root1]; /* 集合1併入集合2 */
S[Root1]=Root2;
}
else
{
S[Root1]+=S[Root2]; /* 集合2併入集合1 */
S[Root2]=Root1;
}
}
/*路徑壓縮函式*/
SetName Find(SetType S,ElementType X)
{ /*預設集合元素全部初始化為-1*/
if (S[X]<0)//找到集合的根
return X;// 返回X所屬的根節點的下標
else
return S[X]=Find(S,S[X]); //1.找到父節點所屬集合的根;2.把根變成X的父節點;3.返回根
}
bool CheckCycle(SetType VSet,Vertex V1,Vertex V2) //檢查連線V1和V2的邊是否在現有的最小生成樹子集中構成迴路
{
Vertex Root1,Root2;
Root1=Find(VSet,V1);//得到V1所屬的連通集名稱
Root2=Find(VSet,V2); //得到V2所屬的連通集名稱
if(Root1==Root2) //如果V1與V2已經連通,這個邊就不能要
return false;
else //如果沒有連通,那麼這條邊就可以被收錄進來,同時將V1、V2併入同一連通集
{
Union(VSet,Root1,Root2);
return true;
}
}
/*-------------------- 並查集定義結束 --------------------*/
/*-------------------- 邊的最小堆定義 --------------------*/
/*快速得到最小邊的函式*/
void PercDown(Edge ESet,int p,int N) //實現對ESet[]中的元素向下遷移調整的過程
{//將N個元素的邊陣列中以ESet[p]為根的子堆調整為關於Weight的最小堆
int Parent,Child;
struct ENode X;
X=ESet[p];//用來取出根節點存放的值
for( Parent=p; (Parent*2+1)<N; Parent=Child )
{ //因為這裡的堆下標從0開始,所以需要讓Child=Parent*2+1
Child=Parent*2+1;
if((Child!=N-1)&&(ESet[Child].Weight>ESet[Child+1].Weight))
Child++; //Child指向左右子節點的較小者
if(X.Weight<=ESet[Child].Weight) //找到合適的位置
break;
else //下濾X
ESet[Parent]=ESet[Child];
}
ESet[Parent]=X;
}
void InitializeESet(LGraph Graph,Edge ESet) //將圖的邊存入陣列ESet,並且初始化為最小堆
{
Vertex V;
PtrToAdjVNode W;
int ECount;
/* 將圖的邊存入陣列ESet */
ECount=0;/* 邊集合初始化為零*/
for(V=0;V<Graph->Nv;V++)
for( W=Graph->G[V].FirstEdge;W;W=W->Next)
if(V<W->AdjV)
{/* 避免重複錄入無向圖的邊,只收V1<V2的邊 */
ESet[ECount].V1=V;
ESet[ECount].V2=W->AdjV;
ESet[ECount++].Weight=W->Weight;
}
/* 初始化為最小堆 */
for(ECount=Graph->Ne/2;ECount>=0;ECount--)
PercDown(ESet,ECount,Graph->Ne);
}
/*交換兩條邊的函式*/
void Swap(Edge a,Edge b)
{
Edge temp=(Edge)malloc(sizeof(struct ENode));
*temp=*a;
*a=*b;
*b=*temp;
free(temp);
}
int GetEdge(Edge ESet,int CurrentSize) //給定當前堆的大小CurrentSize,將當前最小邊位置彈出並調整堆
{ /* 將最小邊與當前堆的最後一個位置的邊交換 */
Swap(&ESet[0],&ESet[CurrentSize-1]);
/* 將剩下的邊繼續調整成最小堆 */
PercDown(ESet,0,CurrentSize-1 );
return CurrentSize-1; /* 返回最小邊所在位置 */
}
/*-------------------- 最小堆定義結束 --------------------*/
LGraph CreateGraph(Vertex MaxNumber) //鄰接表的圖定義
{
Vertex i;
LGraph graph=(LGraph)malloc(sizeof(struct GNode));
graph->Nv=MaxNumber;
graph->Ne=0;
graph->G=(AdjList*)malloc(sizeof(AdjVNode)*MaxNumber);
for(i=0;i<graph->Nv;i++)
{
graph->G[i].FirstEdge=NULL;
}
return graph;
}
void InsertEdge(LGraph MST,Edge edge)
{
PtrToAdjVNode NewNode;
NewNode=(PtrToAdjVNode)malloc(sizeof(struct AdjVNode));
NewNode->AdjV=edge->V2;
NewNode->Weight=edge->Weight;
NewNode->Next=MST->G[edge->V1].FirstEdge;
MST->G[edge->V1].FirstEdge=NewNode;
NewNode=(PtrToAdjVNode)malloc(sizeof(struct AdjVNode));
NewNode->AdjV=edge->V1;
NewNode->Weight=edge->Weight;
NewNode->Next=MST->G[edge->V2].FirstEdge;
MST->G[edge->V2].FirstEdge=NewNode;
}
int Kruskal(LGraph Graph,LGraph MST) //將最小生成樹儲存為鄰接表儲存的圖MST,返回最小權重和
{
WeightType TotalWeight;
int ECount,NextEdge;
SetType VSet;
Edge ESet;
VSet=(SetType)malloc(sizeof(ElementType)*(Graph->Nv));
InitializeVSet(VSet,Graph->Nv);
ESet=(Edge)malloc(sizeof(struct ENode)*(Graph->Ne));
InitializeESet(Graph,ESet);
MST=CreateGraph(Graph->Nv);
TotalWeight=0;
ECount=0;
NextEdge=Graph->Ne;
while (ECount<Graph->Nv-1)
{
NextEdge=GetEdge(ESet,NextEdge);
if(NextEdge<0)
break;
if (CheckCycle(VSet,ESet[NextEdge].V1,ESet[NextEdge].V2)==true)
{
InsertEdge(MST,ESet+NextEdge);
TotalWeight+=ESet[NextEdge].Weight;
ECount++;
}
}
if (ECount<Graph->Nv-1)
TotalWeight=-1;
return TotalWeight;
}
流程圖:
流程圖2 最低成本建設問題主函式
流程圖3 Kruskal演算法
三、 系統使用說明
如何執行你的系統,使用的賬號密碼?執行時用的什麼資料,結果如何?每個過程抓圖實現
1、建設道路數量問題
複製“測試資料”到控制檯,即可看到結果。
這裡給出三組資料,每組資料的第一行為頂點數N和邊數M,
隨後M行給出相連的兩個頂點,最後以0結束程式。
測試資料:
3 2
1 2
1 3
3 0
5 3
1 2
1 3
4 5
0
測試結果:
需要建設道路0條
需要建設道路2條
需要建設道路1條
圖片展示:
2、最低成本建設問題
複製“測試資料”到控制檯,即可看到結果。
這裡給出兩組資料,每組資料的第一行為頂點數N和邊數M,
隨後M行給出相連的兩個頂點以及邊的權值。
表1 最低成本建設暢通工程問題部分測試資料
輸入 |
3 1 2 3 2 |
5 4 1 2 1 2 3 2 3 1 3 4 5 4 |
6 15 1 2 5 1 3 3 1 4 7 1 5 4 1 6 2 2 3 4 2 4 6 2 5 2 2 6 6 3 4 6 3 5 1 3 6 1 4 5 10 4 6 8 5 6 3 |
輸出 |
需要建設更多道路 |
需要建設更多道路 |
最低成本 = 12 |
圖片展示:
四、 問題及解決辦法
除錯過程中遇到的主要問題有哪些?如何解決的。有何結論?請用(1)(2)(3)分別列舉
成員1:
(1) 剛開始準備去寫的。自認為應該不會太難,但是真正去寫的時候才會發現有很多點都沒有注意到,導致出現各種錯誤。有問題的地方就會認真參考課本進行修改。
(2) 主函式如何輸入資料的時候,一開始想著先輸入頂點數、邊數,然後依次輸入0下標開始的每個頂點下標和邊的權值,最後輸入頂點資料。後來感覺沒有必要輸入頂點資料,就用兩個臨時變數接收兩個頂點的值,再把兩個臨時變數分別減1後再傳給InsertEdge函式。
(3) 核心Kruskal函式的實現開始沒有真正理解究竟是如何呼叫其他函式的,導致如何判斷迴路時總是編譯錯誤,後來又認真看了看課本關於Kruskal演算法的具體實現,才有了更加深入的瞭解。
成員2:
(1) 並查集遇到的問題:一開始不確定自己是否能夠十分順利的利用並查集去實現這些功能,也不明白如何去進行並查集的初始化,後來將課本好好翻看了幾遍,對專案有了更深入的理解,找到了並查集的相關內容,瞭解了使用並查集去實現的好處;
(2) 最小堆遇到的問題:一開始忘記了對邊集的初始化,不過後來發現這個問題時就及時改正了;在進行邊的收錄時也是頻頻報錯,總是混淆邊與頂點的關係;在進行最小邊與當前堆的最後一個位置的邊交換時總是不能交換成功,最後想到利用指標去進行交換,解決了這個問題;
(3) 在專案設計和實現過程中,總是會遇到各種各樣的問題,首先要獨立思考,另外,要善於去理解和運用課本程式碼,所謂萬變不離其宗,其實書本把各種主要的程式碼已經提供出來了,我們要自己把這些函式去串起來,組成一個完整的系統。
成員3:
(1) 開始很多細節問題不注意,總是字母錯了或者符號的中英文導致開始時候找不到錯誤在哪。然後一個一個仔細對照發現錯誤並一一改成規範程式碼。
(2) 猶豫不決用圖的鄰接表深度優先搜尋還是用並查集的方法,最後通決定實際執行效果較快的並查集。並查集的使用不是很明確,理解的很表面。因此我翻閱課本相關內容加深印象有了更深的理解,並完成我的相對應部分。
(3) 知識點串聯的並不順也不牢固,這段時間就又重新將知識系統地複習一遍。
五、 專案總結
專案和你的預期相符與否,功能是否均實現?系統的特色在哪兒?還有哪些有待改進的地方?
在專案設計和實現過程中,你收穫了哪些?意識到自己的不足之處有哪些?
請用(1)(2)(3)分別列舉出來。
成員1:
(1) 專案和我的預期完全相符,想要實現的功能也都實現了;
(2) 從剛開始對Kruskal演算法書上表面的理解,到後來從計算機方面深入地瞭解,同時鞏固了我對圖這一章節的知識。
(3) 通過本次專案設計我意識到團隊合作的重要性,各成員之間應該互相交流,在交流的基礎上才可以使得程式更加的完善。
成員二 :
(1) 專案和我的預期是完全相符的,想要實現的功能也都實現了;
(2) 它的特色之處在於利用了一種更為簡單的方法——並查集:將所有相連的結點都併入同一個集合,最後數一下集合個數即可。路徑壓縮不僅讓我複習了以前地知識,也優化了並查集大大提高了效率;
(3) 此外,另一個特色之處就是運用了維護一個關於邊權重的最小堆,每次從堆中取出最小元的方法。這樣做的好處是不需要對全部M條邊進行排序,在最好情況下,如果結果需要的N-1條邊都排在最前面,我們只需要O(NlogM)的時間就可以得到結果;在最壞情況下,這種方法與排序的方法持平。所以我選擇了利用最小堆實現GetEdge函式;
(4) 在專案設計和實現過程中,我意識到了團隊協作的重要性,當你的程式碼多次報錯自己卻找不到問題所在時可以和隊友進行交流,因為大家對專案都有自己的理解,你的方法行不通時,那別人的想法可能就會給你一個大的啟發。
成員3:
(1) 專案和我的預期是完全相符的,想要實現的功能也都實現了;
(2) 系統的特色在於使用並查集統計連通集個數。
(3) 我的收穫就是通過著這個專案過程又重新系統的複習了相關知識並學會了之前沒有那麼明白的問題。並且我意識到了團隊協作的重要性,我的程式碼多次報錯卻找不到問題所在時是和隊友進行交流,發現那些細節錯誤。
(4) 我的不足是馬虎不認真導致犯一些很不該犯的錯誤。
參考文獻:
【2】陳越主編,何欽銘等編著 . 資料結構 . 北京:高等教育出版社 . 2016.6(2019.12重印) .
p159~p163 p211~214 p301~308