在這一專輯(最小生成樹)中的上一期講到了prim算法,但是prim算法比較難懂,為了避免看不懂,就先用kruskal算法寫題吧,下面將會將三道例題,加一道變形,以及一道大水題,水到不用高級數據結構,建樹,畫圖,最短路徑什么的,統統不需要。廢話不多說,直接看題:
1.例題精講
T1:
1348:【例4-9】城市公交網建設問題
時間限制: 1000 ms 內存限制: 65536 KB
提交數: 2094 通過數: 650
【題目描述】
有一張城市地圖,圖中的頂點為城市,無向邊代表兩個城市間的連通關系,邊上的權為在這兩個城市之間修建高速公路的造價,研究后發現,這個地圖有一個特點,即任一對城市都是連通的。現在的問題是,要修建若干高速公路把所有城市聯系起來,問如何設計可使得工程的總造價最少?
【輸入】
n(城市數,1<≤n≤100)
e(邊數)
以下e行,每行3個數i,j,wiji,j,wij,表示在城市i,j之間修建高速公路的造價。
【輸出】
n-1行,每行為兩個城市的序號,表明這兩個城市間建一條高速公路。
【輸入樣例】
5 8 1 2 2 2 5 9 5 4 7 4 1 10 1 3 12 4 3 6 5 3 3 2 3 8
【輸出樣例】
1 2 2 3 3 4 3 5
看完之后有思路嗎?這題肯定簡單,這是一道純模板題,只不過輸出有點麻煩。總之,先來回憶一下kruskal算法原理:首先需要找到圖中的一條最短的邊,如果它不與最小生成樹集合中的其他邊產生回路,那么就加入這條邊至集合中,上次小編寫的很草率,只是一個偽代碼(偽代碼如下),這次的題目小編會寫成正式代碼;接着,輸出又是一個麻煩事,這就需要分析樣例了,先好好看一看樣例,你發現了嗎?左邊的數總小於右邊的數,下面的第一個數總比上面的第一個數大,當然,如果第一個數一樣大,那么就按第二個數從小到大排序。依據這個規律,接着,就來看一看AC代碼吧。
1 //假設MST[]是最小生成樹的集合,cnt表示是存入集合的邊數 2 while(cnt<n-1)//共有n-1條邊 3 { 4 在圖中找出最短的一條邊; 5 if(添加這條邊不產生回路) 6 { 7 加入MST集合; 8 cnt++; 9 } 10 }
//偽代碼
1 #include<iostream> 2 #include<cstdio> 3 #include<queue> 4 #include<cmath> 5 #include<algorithm> 6 using namespace std; 7 int n,e,a[1000],k,p,q; 8 struct tree{ 9 int start; 10 int end; 11 int cost; 12 }; 13 tree T[1000]; 14 bool operator < (const tree& a,const tree& b)//按cost的值從小到大排序 15 { 16 return a.cost>b.cost; 17 } 18 bool cmp(tree a,tree b)//這個排序方式就是上面所說的關系 19 { 20 if(a.start==b.start) 21 return a.end<b.end; 22 return a.start<b.start; 23 } 24 priority_queue<tree>t; 25 inline int find(int x)//並查集 26 { 27 if(x==a[x]) return x; 28 else return a[x]=find(a[x]); 29 } 30 void kruskal() 31 { 32 for(int i=1;i<=n;i++)//並查集初始化 33 a[i]=i; 34 tree large; 35 for(int i=1;i<=e;i++) 36 { 37 large=t.top();//獲取最小邊 38 t.pop(); 39 if(find(large.start)!=find(large.end))//如果不產生回路 40 { 41 p=find(large.start);q=find(large.end); 42 a[q]=p; 43 T[++k]=large;//加入到集合中 44 } 45 } 46 sort(T+1,T+k+1,cmp);//按規律排序,否則順序不對 47 for(int i=1;i<=k;i++) 48 printf("%d %d \n",T[i].start,T[i].end); 49 } 50 int main() 51 { 52 tree s; 53 scanf("%d%d",&n,&e); 54 for(int i=1;i<=e;i++) 55 { 56 scanf("%d%d%d",&s.start,&s.end,&s.cost); 57 if(s.start>s.end) swap(s.start,s.end); 58 t.push(s); 59 } 60 kruskal(); 61 return 0; 62 }
//AC代碼
這個代碼雖然看起來很長,但是效率很高,如果用數組來存儲,代碼確實精簡了,看起來確實易懂了,但是很浪費時間,每一次的最小邊都要花O(n)的時間去尋找,如果用堆(優先隊列),直接詢問隊頂元素就可以了。如果你並不清楚用着結構體的優先隊列,就一定不會理解以下這段代碼的意思,這段代碼表示按cost的值進行排序,因為結構體中有三個元素,不這么寫就無法按你的心意排序。
14 bool operator < (const tree& a,const tree& b) 15 { 16 return a.cost>b.cost; 17 }
T2:
1349:【例4-10】最優布線問題
時間限制: 1000 ms 內存限制: 65536 KB
提交數: 1228 通過數: 733
【題目描述】
學校有n台計算機,為了方便數據傳輸,現要將它們用數據線連接起來。兩台計算機被連接是指它們有數據線連接。由於計算機所處的位置不同,因此不同的兩台計算機的連接費用往往是不同的。
當然,如果將任意兩台計算機都用數據線連接,費用將是相當龐大的。為了節省費用,我們采用數據的間接傳輸手段,即一台計算機可以間接的通過若干台計算機(作為中轉)來實現與另一台計算機的連接。
現在由你負責連接這些計算機,任務是使任意兩台計算機都連通(不管是直接的或間接的)。
【輸入】
第一行為整數n(2≤n≤100),表示計算機的數目。此后的n行,每行n個整數。第x+1行y列的整數表示直接連接第x台計算機和第y台計算機的費用。
【輸出】
一個整數,表示最小的連接費用。
【輸入樣例】
3 0 1 2 1 0 1 2 1 0
【輸出樣例】
2
這道題和剛才的題出自同一個方法,似乎用prim算法更好,不知道你剛才的代碼還留着嗎?其實剛才的代碼稍加改動,這道題就能過了,下面來分析一下這道題與剛才的題的異同點,首先輸入一定是不同的,要輸入一個矩陣,所以這里要改一下;還有這道題只求和,不求每一條邊,這可是一個福利,不用像剛才一樣麻煩了,總之,廢話不多說,AC代碼呈上:
1 #include<iostream> 2 #include<cstdio> 3 #include<queue> 4 #include<cmath> 5 #include<algorithm> 6 using namespace std; 7 int n,e,a[1000],k,p,q,ans,map[1000][1000]; 8 struct tree{ 9 int start; 10 int end; 11 int cost; 12 }; 13 //tree T[1000]; 14 bool operator < (const tree& a,const tree& b) 15 { 16 return a.cost>b.cost; 17 } 18 bool cmp(tree a,tree b) 19 { 20 if(a.start==b.start) 21 return a.end<b.end; 22 return a.start<b.start; 23 } 24 priority_queue<tree>t; 25 inline int find(int x) 26 { 27 if(x==a[x]) return x; 28 else return a[x]=find(a[x]); 29 } 30 void kruskal() 31 { 32 for(int i=1;i<=n;i++) 33 a[i]=i; 34 tree large; 35 for(int i=1;i<=e;i++) 36 { 37 large=t.top(); 38 t.pop(); 39 if(find(large.start)!=find(large.end)) 40 { 41 p=find(large.start);q=find(large.end); 42 a[q]=p; 43 ans+=large.cost; 44 } 45 } 46 } 47 int main() 48 { 49 tree s; 50 scanf("%d",&n); 51 for(int i=1;i<=n;i++) 52 for(int j=1;j<=n;j++) 53 { 54 scanf("%d",&map[i][j]); 55 s.start=i; 56 s.end=j; 57 s.cost=map[i][j]; 58 t.push(s); 59 if(i!=j) 60 e++; 61 } 62 kruskal(); 63 printf("%d",ans); 64 return 0; 65 }
具體就不怎么介紹了,這也是一道純模板題,如果你想練手,可以先寫一寫下面這道題。
T3:
1350:【例4-11】最短網絡(agrinet)
時間限制: 1000 ms 內存限制: 65536 KB
提交數: 1054 通過數: 711
【題目描述】
農民約翰被選為他們鎮的鎮長!他其中一個競選承諾就是在鎮上建立起互聯網,並連接到所有的農場。當然,他需要你的幫助。約翰已經給他的農場安排了一條高速的網絡線路,他想把這條線路共享給其他農場。為了用最小的消費,他想鋪設最短的光纖去連接所有的農場。你將得到一份各農場之間連接費用的列表,你必須找出能連接所有農場並所用光纖最短的方案。每兩個農場間的距離不會超過100000。
【輸入】
第一行:農場的個數,N(3≤N≤100)。
第二行..結尾:后來的行包含了一個N*N的矩陣,表示每個農場之間的距離。理論上,他們是N行,每行由N個用空格分隔的數組成,實際上,他們限制在80個字符,因此,某些行會緊接着另一些行。當然,對角線將會是0,因為不會有線路從第i個農場到它本身。
【輸出】
只有一個輸出,其中包含連接到每個農場的光纖的最小長度。
【輸入樣例】
4 0 4 9 21 4 0 8 17 9 8 0 16 21 17 16 0
【輸出樣例】
28
怎么樣,你寫出來了嗎?下面是AC代碼:
1 #include<iostream> 2 #include<cstdio> 3 #include<queue> 4 #include<cmath> 5 #include<algorithm> 6 using namespace std; 7 int n,e,a[1000],k,p,q,ans,map[1000][1000]; 8 struct tree{ 9 int start; 10 int end; 11 int cost; 12 }; 13 //tree T[1000]; 14 bool operator < (const tree& a,const tree& b) 15 { 16 return a.cost>b.cost; 17 } 18 bool cmp(tree a,tree b) 19 { 20 if(a.start==b.start) 21 return a.end<b.end; 22 return a.start<b.start; 23 } 24 priority_queue<tree>t; 25 inline int find(int x) 26 { 27 if(x==a[x]) return x; 28 else return a[x]=find(a[x]); 29 } 30 void kruskal() 31 { 32 for(int i=1;i<=n;i++) 33 a[i]=i; 34 tree large; 35 for(int i=1;i<=e;i++) 36 { 37 large=t.top(); 38 t.pop(); 39 if(find(large.start)!=find(large.end)) 40 { 41 p=find(large.start);q=find(large.end); 42 a[q]=p; 43 ans+=large.cost; 44 } 45 } 46 } 47 int main() 48 { 49 tree s; 50 scanf("%d",&n); 51 for(int i=1;i<=n;i++) 52 for(int j=1;j<=n;j++) 53 { 54 scanf("%d",&map[i][j]); 55 s.start=i; 56 s.end=j; 57 s.cost=map[i][j]; 58 t.push(s); 59 if(i!=j) 60 e++; 61 } 62 kruskal(); 63 printf("%d",ans); 64 return 0; 65 }
有沒有發現什么?和T2一模一樣的代碼竟然在T3就過了,我猜出這道題的是為了讓我們再手敲一遍代碼加深理解,既然這道題是披着T3皮的T2,那就不解釋了,直接看一道變形。
2.牛刀小試
T4:
1391:局域網(net)
時間限制: 1000 ms 內存限制: 65536 KB
提交數: 1072 通過數: 658
【題目描述】
某個局域網內有n(n≤100)台計算機,由於搭建局域網時工作人員的疏忽,現在局域網內的連接形成了回路,我們知道如果局域網形成回路那么數據將不停的在回路內傳輸,造成網絡卡的現象。因為連接計算機的網線本身不同,所以有一些連線不是很暢通,我們用f(i,j)表示i,j之間連接的暢通程度(f(i,j)≤1000),f(i,j)值越小表示i,j之間連接越通暢,f(i,j)為0表示i,j之間無網線連接。現在我們需要解決回路問題,我們將除去一些連線,使得網絡中沒有回路,並且被除去網線的Σf(i,j)最大,請求出這個最大值。
【輸入】
第一行兩個正整數n k
接下來的k行每行三個正整數i j m表示i,j兩台計算機之間有網線聯通,通暢程度為m。
【輸出】
一個正整數,Σf(i,j)的最大值。
【輸入樣例】
5 5 1 2 8 1 3 1 1 5 3 2 4 5 3 4 2
【輸出樣例】
8
照之前的套路小編題也不看就猜到了要干什么,瀟灑的把T1和T2結合在一起,稍微一改輸入輸出,差點就提交了,幸虧最后試了一下輸入樣例,什么,竟然之前的模板不靈了!小編看了看題,才恍然大悟,也不過如此,這道題求的是產生回路的邊的和,那么就加產生回路的邊好了。AC代碼如下:
1 #include<iostream> 2 #include<cstdio> 3 #include<queue> 4 #include<cmath> 5 #include<algorithm> 6 using namespace std; 7 int n,e,a[1000],k,p,q,ans; 8 struct tree{ 9 int start; 10 int end; 11 int cost; 12 }; 13 tree T[1000]; 14 bool operator < (const tree& a,const tree& b) 15 { 16 return a.cost>b.cost; 17 } 18 bool cmp(tree a,tree b) 19 { 20 if(a.start==b.start) 21 return a.end<b.end; 22 return a.start<b.start; 23 } 24 priority_queue<tree>t; 25 inline int find(int x) 26 { 27 if(x==a[x]) return x; 28 else return a[x]=find(a[x]); 29 } 30 void kruskal() 31 { 32 for(int i=1;i<=n;i++) 33 a[i]=i; 34 tree large; 35 for(int i=1;i<=e;i++) 36 { 37 large=t.top(); 38 t.pop(); 39 if(find(large.start)!=find(large.end)) 40 { 41 p=find(large.start);q=find(large.end); 42 a[q]=p; 43 44 } 45 else ans+=large.cost;//注意這里有變 46 } 47 } 48 int main() 49 { 50 tree s; 51 scanf("%d%d",&n,&e); 52 for(int i=1;i<=e;i++) 53 { 54 scanf("%d%d%d",&s.start,&s.end,&s.cost); 55 if(s.start>s.end) swap(s.start,s.end); 56 t.push(s); 57 } 58 kruskal(); 59 printf("%d",ans); 60 return 0; 61 }
這道題輕松一變就過了,可以說這四道題是捆綁在一起的,一道過了,四道稍變就全過。其實例題一共有四道,但是為什么沒有把第四到放進來講呢?這是因為這道題是到大水題。
3.大水題
T5:
1351:【例4-12】家譜樹
時間限制: 1000 ms 內存限制: 65536 KB
提交數: 826 通過數: 568
【題目描述】
有個人的家族很大,輩分關系很混亂,請你幫整理一下這種關系。
給出每個人的孩子的信息。
輸出一個序列,使得每個人的后輩都比那個人后列出。
【輸入】
第1行一個整數N(1≤N≤100),表示家族的人數;
接下來N行,第I行描述第I個人的兒子;
每行最后是0表示描述完畢。
【輸出】
輸出一個序列,使得每個人的后輩都比那個人后列出;
如果有多解輸出任意一解。
【輸入樣例】
5 0 4 5 1 0 1 0 5 3 0 3 0
【輸出樣例】
2 4 5 3 1
這道題怎么辦?似乎找不到和最小生成樹的關系,和圖似乎有點關系,可是怎么寫呢?這又是一個迷,先看代碼再解釋:
1 #include<iostream> 2 #include<cstdio> 3 using namespace std; 4 int n,a[1000][1000],ans[1000],cnt,w[1000],k,minn;bool vis[1000]={false}; 5 int main() 6 { 7 scanf("%d",&n); 8 for(int i=1;i<=n;i++) 9 for(int j=1;;j++) 10 { 11 scanf("%d",&a[i][j]); 12 if(a[i][j]==0) break;w[i]++; 13 } 14 for(int i=1;i<=n;i++) 15 { 16 minn=999999,k=0; 17 for(int j=1;j<=n;j++) 18 { 19 if(vis[j]==true) continue; 20 if(w[j]<minn) 21 { 22 k=j; 23 minn=w[j]; 24 } 25 } 26 vis[k]=true;ans[++cnt]=k; 27 for(int j=1;j<=n;j++) 28 { 29 if(vis[j]==true) continue; 30 for(int l=1;l<=n;l++) 31 if(a[j][l]==k) w[j]--; 32 } 33 } 34 for(int i=cnt;i>=1;i--) 35 printf("%d ",ans[i]); 36 return 0; 37 }
對,你沒有看錯,它雖然身處最小生成樹中,但是卻不需要高級數據結構,說白了就是個找規律題,小編也是瞎貓碰見死耗子,隨便一腦洞大開就AC了,當然,也歡迎用最小生成樹寫出的大佬評論感受,下面來講一講小編找出的規律:首先,題目告訴要把父輩排在子輩前面,小編想到了用並查集,但是貌似寫不出來,於是小編就猜想是不是兒子越多,輩分就越高,那么如果兒子一樣多又該怎么處理呢?接着,小編想了各種方法處理,最后突然想到可以讓他們減少兒子,那又怎么減呢?把已經定位好輩分的人從剩下人的兒子中抹掉,假裝其他人都失去了這個兒子。那么是應該先挑小輩呢?還是先挑長輩呢?當然是先挑小輩,因為比較好選,優先選長輩會出現很多奇怪的狀況,小編親手試過,有了這個大思路,小編就以試試的心態寫出了這個代碼,沒想到竟然過了,這道題不需要用樹,不需要圖,不需要最小生成樹竟然可以過,純找規律,真是道大水題。
原本小編還是一知半解的,寫了幾道題以后終於弄明白了,建議大家也可以寫一寫。
4.附測評網站:
5.局域網(net)
專欄:
【數據結構】 最小生成樹(一)——什么是最小生成樹?
【數據結構】 最小生成樹(二)——kruskal算法
【數據結構】 最小生成樹(三)——prim算法
【數據結構】 最小生成樹(四)——利用kruskal算法搞定例題×3+變形+一道大水題