還是暢通工程
Time Limit: 4000/2000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)
Problem Description
某省調查鄉村交通狀況,得到的統計表中列出了任意兩村莊間的距離。省政府“暢通工程”的目標是使全省任何兩個村莊間都可以實現公路交通(但不一定有直接的公路相連,只要能間接通過公路可達即可),並要求鋪設的公路總長度為最小。請計算最小的公路總長度。
Input
測試輸入包含若干測試用例。每個測試用例的第1行給出村莊數目N ( < 100 );隨後的N(N-1)/2行對應村莊間的距離,每行給出一對正整數,分別是兩個村莊的編號,以及此兩村莊間的距離。為簡單起見,村莊從1到N編號。
當N為0時,輸入結束,該用例不被處理。
Output
對每個測試用例,在1行裡輸出最小的公路總長度。
Sample Input
3
1 2 1
1 3 2
2 3 4
4
1 2 1
1 3 4
1 4 1
2 3 3
2 4 2
3 4 5
0
Sample Output
3
5
HintHint
Huge input, scanf is recommended.
在無向帶權連通圖G中,如果一個連通子樹包含所有頂點,並且連接這些頂點的邊權之和最小,
那麼這個連通子圖就是G的最小生成樹。求最小生成樹的一個常見算法是Prim算法。
prim算法(時間復雜度為O(n^3)):
Prim算法的基本思想是:
1)設置兩個集合V和S,任意選擇一個頂點作為起始頂點,將起始頂點放入集合S,其余頂點存入集合
V中;2)然後使用貪心策略,選擇一條長度最短並且端點分別在S和V中邊(即為最小生成樹的中的一條
邊),將這條邊在V中的端點加入到集合S中;3)循環執行第2)步直到S中包含了所有頂點。
#include<stdio.h> #include<string.h> #define inf 0x3f3f3f3f int map[100][100],s[100],vis[100]; int n,m; int prim() { int i,j,t,p,min,minpos,cnt; int ans=0; cnt=0; /*記錄已經加入的點的個數*/ vis[1]=1; /*從第一個點開始找*/ s[cnt++]=1; /*s數組保存已經加入的點*/ while(cnt<n) /*點還沒有加入完*/ { t=cnt; min=inf; for(i=0;i<t;i++) { p=s[i]; for(j=1;j<=n;j++) { if(!vis[j]&&map[p][j]<min) /*在已經加入的點和沒加入的點之間找出一條最短路,*/ { min=map[p][j]; minpos=j; /*記錄下新找到的最短路的端點*/ } } } ans+=min; s[cnt++]=minpos; /*更新已經加入的點*/ vis[minpos]=1; } return ans; } int main() { int u,v,w,i; while(~scanf("%d",&n)&&n) { m=n*(n-1)/2; memset(vis,0,sizeof(vis)); memset(map,inf,sizeof(map)); for(i=0;i<m;i++) { scanf("%d%d%d",&u,&v,&w); map[u][v]=w; map[v][u]=w; } int sum=prim(); printf("%d\n",sum); } return 0; }
上面的算法有三個循環,時間復雜度為O(N^3),考慮到由於使用的是貪心策略,則每添加一個新頂點到集合S中的時候,才會改變V中每個點到S中的點的最小邊的長度。因此可以用一個數組nearest[N](N為頂點個數)記錄在生成最小數的過程中,記錄V中每個點的到S中點的最小邊長,用另外一個數組adj[N]記錄使得該邊最小的對應的鄰接點。那麼O(N)的時間了找到最短的邊,並且能在O(N)的時間裡更新nearest[N]和adj[N]。因此可以得到O(N^2)的算法。
#include<stdio.h> #include<string.h> #define inf 0x3f3f3f3f int map[100][100]; int n,m; /*記當前生成樹的節點集合為S,未使用的節點結合為V*/ int vis[100]; //標記某個點是否在S中 int adj[100]; //記錄與S中的點最接近的點 int nearest[100]; //記錄V中每個點到S中的點的最短邊 int prim() { int i,j,min; int ans=0; vis[1]=1; for(i=2;i<=n;i++) { nearest[i]=map[1][i]; adj[i]=1; } int cnt=n-1; /*記錄邊的條數*/ while(cnt--) { min=inf; j=1; for(i=1;i<=n;i++) { if(!vis[i]&&nearest[i]<min) { min=nearest[i]; j=i; } } ans+=map[j][adj[j]]; vis[j]=1; for(i=1;i<=n;i++) { if(!vis[i]&&map[i][j]<nearest[i]) { nearest[i]=map[i][j]; /*找最短的邊*/ adj[i]=j; /*找最接近的點*/ } } } return ans; } int main() { int i,sum,u,v,w; while(~scanf("%d",&n)&&n) { memset(vis,0,sizeof(vis)); memset(map,0,sizeof(map)); m=n*(n-1)/2; for(i=0;i<m;i++) { scanf("%d%d%d",&u,&v,&w); map[u][v]=map[v][u]=w; } sum=prim(); printf("%d\n",sum); } return 0 ; }
Kruskal算法(時間復雜度O(ElogE),E為邊數):
給定無向連同帶權圖G = (V,E),V = {1,2,...,n}。Kruskal算法構造G的最小生成樹的基本思想是:
(1)首先將G的n個頂點看成n個孤立的連通分支。將所有的邊按權從小大排序。
(2)從第一條邊開始,依邊權遞增的順序檢查每一條邊。並按照下述方法連接兩個不同的連通分支:當查看到第k條邊(v,w)時,如果端點v和w分別是當前兩個不同的連通分支T1和T2的端點是,就用邊(v,w)將T1和T2連接成一個連通分支,然後繼續查看第k+1條邊;如果端點v和w在當前的同一個連通分支中,就直接再查看k+1條邊。這個過程一個進行到只剩下一個連通分支時為止。
此時,已構成G的一棵最小生成樹。
#include<stdio.h> #include<string.h> #include<algorithm> using namespace std; int father[100]; int n,m; struct point { int u; int v; int w; }a[5000]; bool comp(point a1,point a2) /*按權值從小到大排序*/ { return a1.w<a2.w; } void initial() /*並查集初始化*/ { for(int i=0;i<=100;i++) father[i]=i; } int find(int x) /*查找根節點*/ { if(father[x]==x) return x; return find(father[x]); } void merge(int p,int q) /*合並兩個集合*/ { int pp=find(p); int qq=find(q); if(pp!=qq) { if(pp<qq) father[qq]=pp; else father[pp]=qq; } } int kruskal() { initial(); /*初始化*/ int ans=0; sort(a+1,a+m+1,comp); /*排序*/ for(int i=1;i<=m;i++) { int x=find(a[i].u); int y=find(a[i].v); if(x!=y) /*兩端點不屬於同一集合*/ { ans+=a[i].w; merge(x,y); /*合並*/ } } return ans; } int main() { int i,sum; while(~scanf("%d",&n)&&n!=0) { m=n*(n-1)/2; for(i=1;i<=m;i++) scanf("%d%d%d",&a[i].u,&a[i].v,&a[i].w); sum=kruskal(); printf("%d\n",sum); } return 0; }