還是暢通工程
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;
}