最小生成樹(prim和kruskal)
問題引入:
假設要在n個城市之間建立通訊網路,則連通n個城市至少需要n-1條線路。n個城市之間,最多可能設定n*(n-1)/2條線路。這時,自然會考慮一個問題:如何在這些可能的線路中選擇n-1條,使得在最節省費用的前提下建立該網路通訊?
解決方法:
1.prim演算法
演算法思想:
G=(V,E)是無向連通帶權圖,V=(1,2,3,…,n);設最小生成樹T=(U,TE),演算法結束時U=V,TE屬於E。構造最小生成樹T的prim演算法思想是:首先令U={u0},u0屬於V,TE={}。然後,只要U是V的真子集,就做如下貪心選擇:選取滿足條件i屬於U,j屬於V-U,且邊(i,j)是連線U和V-U的所有邊中的最短邊,即該邊的權值最小。然後,將頂點j加入集合U,邊(i,j)加入結合TE。繼續上面的貪心選擇一直到U=V為止,此時選取到的所有的邊恰好構成G的一棵最小生成樹T。(看成是一個集合到另外一個集合,逐次從中選出一個路線)
演算法設計
- 將資料存放在陣列,如果兩個點不通,則設定為無窮大(0x33f3f3f)
- 初始化陣列lowcost[],假設從1號點(下標1,lowcost[0]=0)開始,將與1相連的邊存放到陣列lowcost[]中
- 尋找到與1相連的最小的邊,記錄其在lowcost陣列中的小標和權值,
#include<cstdio>
#include<cstring>
using namespace std;
int lou[506][506],v,e,total;
const int inf=0x3f3f3f3f;
void prim()
{
int lowcost[506],flag,minn;//用於記錄每次加入一個點後連通其他點的最小權值,
lowcost[1]=0;
for(int i=2;i<=v;i++)//假設加入了點1,然後給lowcost賦值與1連線的邊權值
{
lowcost[i]=lou[1][i];
}
for(int i=1;i<v;i++)//只需要尋找n-1條邊
{
flag=0,minn=inf;
for(int j=1;j<=v;j++)//找出當前的點集合中最小的邊
{
if(lowcost[j]&&lowcost[j]<minn)
{
minn=lowcost[j];//記錄最小的權值
flag=j;//標記位置
}
}
if(flag!=0)
total+=minn;
lowcost[flag]=0;
for(int i=2;i<=v;i++) //更新lowcost[]陣列
{
if(lowcost[i]&&lowcost[i]>lou[flag][i])
{
lowcost[i]=lou[flag][i];
}
}
}
}
int main()
{
int n,out[506],a,b,c;
scanf("%d",&n);
while(n--)
{
memset(out,0,sizeof(out));
memset(lou,inf,sizeof(lou));//開始賦初值為整型無窮大量
scanf("%d%d",&v,&e);
total=0;
for(int i=0;i<e;i++)
{
scanf("%d%d%d",&a,&b,&c);
lou[a][b]=c;
lou[b][a]=c;
}
prim();
int sm=inf;
for(int i=0;i<v;i++)
{
scanf("%d",&out[i]);
if(sm>out[i])sm=out[i];
}
printf("%d\n",total+sm);
}
return 0;
}
2.kruskal演算法
演算法思想:
kruskal演算法將n個點看成是n個孤立的連通分支。它首將所有的邊按權從小到大排序。然後,只要T中的連通分支數目不為1,就做如下貪心選擇:在邊集E中選取權值最小的邊(i,j),如果將邊(i,j)加入集合TE中不產生迴路(環),則將邊(i,j)加入邊集TE中,即用邊(i,j)將這兩個連通分支合併連線成一個連通分支,否則繼續選擇下一條最短邊。在這兩種情況下,把邊(i,j)從集合中刪除。繼續上面的貪心選擇知道T中的所有頂點都在同一個連通分支上為止。此時,選取到的n-1條邊恰好構成G的一棵最下生成樹T。
演算法設計:
- 初始化,將圖G的邊集E中的所有邊按權從大到小排序,邊集TE={},把每個頂點都初始化為一個孤立的分支,即一個頂點對應的集合。
- 在E中尋找權值最小的邊(i,j)。
- 如果頂點i和j位於兩個不同的連通分支,則將邊(i,j)加入邊集TE,並執行合併操作將兩個連通分支進行合併。
- 將邊(i,j)從集合E中刪去,即E=E-{(i,j)}
- 如果連通分支數目不為1,轉步驟2;否則演算法結束,生成最小生成樹T。
程式碼:
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
int fa[2000];
struct building
{
int a,b,cost;
}build[130000];
bool cmp(building x,building y)//讓點之間的距離按照花費從小到大排序
{
return x.cost<y.cost;
}
int main()
{
int t,v,e;
scanf("%d",&t);
while(t--)
{
int wei[506],weight=0;
memset(build,0,sizeof(build));
memset(fa,0,sizeof(fa));
scanf("%d%d",&v,&e);
for(int i=0;i<e;i++)
{
scanf("%d%d%d",&build[i].a,&build[i].b,&build[i].cost);
}
sort(build,build+e,cmp);//用sort排序
// for(int j=0;j<e;j++)
// {
// printf("%d %d\n",build[j].a,build[j].cost);
// }
for(int i=0;i<v;i++)
{
scanf("%d",&wei[i]);
fa[i]=i;
}
sort(wei,wei+v);
for(int k=0;k<e;k++)
{
int x=fa[build[k].a-1];//此處也不是很明白
int y=fa[build[k].b-1];
if(x!=y)//當前最小邊,如果不為環,那麼加入該集合
{
weight+=build[k].cost;
//printf("====weight=%d\n",weight);
for(int i=0;i<v;i++)
{
if(fa[i]==y)//把可以相連的點併入一個集合,通過設定它們的fa[]值相同實現
{
fa[i]=x;
}
}
}
}
printf("%d\n",wei[0]+weight);
}
return 0;
}
如果G中的邊比較少,可以採用kruskal,如果邊數較多,則適用prim演算法。時間上,kruskal時間複雜度為O(eloge),prim演算法時間複雜度為O(n^2),prim適用於稠密圖,kruskal適用於稀疏圖。
另外kruskal演算法另外一種通過並查集的方式寫
並查集:
我們可以把每個連通分量看成一個集合,該集合包含了連通分量的所有點。而具體的連通方式無關緊要,好比集合中的元素沒有先後順序之分,只有“屬於”與“不屬於”的區別。圖的所有連通分量可以用若干個不相交集合來表示。
而並查集的精妙之處在於用數來表示集合。如果把x的父結點儲存在p[x]中(如果沒有父親,p[x]=x),則不難寫出結點x所在樹的遞迴程式:
find(int x) {return p[x]==x?x:p[x]=find(p[x]);}
意思是,如果p[x]=x,說明x本身就是樹根,因此返回x;否則返回x的父親p[x]所在樹的根結點。
既然每棵樹表示的只是一個集合,因此樹的形態是無關緊要的,並不需要在“查詢”操作之後保持樹的形態不變,只要順便把遍歷過的結點都改成樹根的兒子,下次查詢就會快很多了
程式碼:
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
int fa[2000];
struct building
{
int a,b,cost;
}build[130000];
bool cmp(building x,building y)
{
return x.cost<y.cost;
}
int find(int x)
{
//printf("-->%d\n",x);
if(x!=fa[x-1])fa[x-1]=find(fa[x-1]);//並查集
return fa[x-1];
}
int main()
{
int t,v,e;
scanf("%d",&t);
while(t--)
{
int wei[506],weight=0;
memset(build,0,sizeof(build));
memset(fa,0,sizeof(fa));
scanf("%d%d",&v,&e);
for(int i=0;i<e;i++)
{
scanf("%d%d%d",&build[i].a,&build[i].b,&build[i].cost);
}
sort(build,build+e,cmp);
// for(int j=0;j<e;j++)
// {
// printf("%d %d\n",build[j].a,build[j].cost);
// }
for(int i=0;i<v;i++)
{
scanf("%d",&wei[i]);
fa[i]=i+1;
}
sort(wei,wei+v);
for(int k=0;k<e;k++)
{
int x=find(build[k].a);
int y=find(build[k].b);
//printf("%d %d\n",x,y);
if(x!=y)
{
weight+=build[k].cost;
fa[x-1]=y;
// printf("%d\n",weight);
}
}
printf("%d\n",wei[0]+weight);
}
return 0;
}