最小生成树(prim算法)

在实际生活中我们常常会遇到这样一些问题:有若干个需要连接的点(不妨假设为一些村庄)和若干条连接着两个点的边(在村庄间修公路),而这些边会有不同的权值(可设为修路所需的费用不同)。现在要连通这些所有的点,并使权值和最小。这类问题在现实生活中很广泛,如修公路、架设电网,等等。
在信息学竞赛中,这种问题有专门的称谓“最小生成树”(Minimum Spanning Tree,简称MST)。
今天介绍一下MST问题中的Prim算法。首先来看一道例题。
题目描述: 
Bessie受雇来到John的农场帮他们建立internet网络。农场有 N (2(= N (= 1, 000)牛棚,编号为1. . N. John之前已经勘测过,发现有 M (1<=m<= 20, 000) 条可能的连接线路,一条 线路是连接某两个牛棚的。 每条可能的线路都有一个建 设如C (1<= C <=100,000). John当然想花尽量少的钱,甚至克扣Bessie的 工资。 
Bessie发现了这点,很生气,决定给John捣乱。她要 选择)些线路组成网, 但费用却尽可能大。当然网络要能正常工作,也就是 任意两个牛棚之间都是相互 可以连通的,并且网络上不能有坏,不然John会很容易发现的。 
请计算组建这种网络最多可能的费用。 
输入文件cowtract. in 
第一行:两个整数N M 
下面M行:每行3个整数A, B, C。表示一个可能的线路要连接A、B两个牛棚, 费用是C。
输出文件cowtract.out 
只一行,一个整数,即花费最大的费用。如果不可能连接通所有牛棚,输出-1。 
输入样例:
5 8
1 2 3
1 3 7
2 3 10
2 4 4
2 5 8
3 4 6
3 5 2
4 5 17
输出样例:
42
注释:
17+8+10+7=42
通过题目中红色粗体字部分容易看出,本题是一道生成树问题。不过有点不同,是“最大生成树”。当然,算法一样。
现在,介绍一下Prim算法的思想。
  1. 首先我们可以确定,对于有N个顶点的图,MST一定只有N-1条边。如果有多余的边,显然会造成浪费。
  2. 将图分为两个集合A和B,设A就是我们要求的MST。其中A一开始只有一个任意的顶点。(整个图最终是连通的,因而该顶点最后必定在MST中,可取任意一点。)
  3. 每次取A和B的最小权交叉边e(即e=(x,y),x∈A且y∈B),将y加入A中。
  4. 当A中有N个顶点时,结束;否则转3。
举个例子:
最小生成树(prim算法)_第1张图片
此为原始的加权连通图。每条边一侧的数字代表其权值。
 
最小生成树(prim算法)_第2张图片
顶点D被任意选为起始点。顶点A、B、E和F通过单条边与D相连。A是距离D最近的顶点,因此将A及对应边AD以高亮表示。
最小生成树(prim算法)_第3张图片
下一个顶点为距离D或A最近的顶点。B距D为9,距A为7,E为15,F为6。因此,F距D或A最近,因此将顶点F与相应边DF以高亮表示。
最小生成树(prim算法)_第4张图片
算法继续重复上面的步骤。距离A为7的顶点B被高亮表示。
最小生成树(prim算法)_第5张图片
在当前情况下,可以在C、E与G间进行选择。C距B为8,E距B为7,G距F为11。点E最近,因此将顶点E与相应边BE高亮表示。 
最小生成树(prim算法)_第6张图片
这里,可供选择的顶点只有C和G。C距E为5,G距E为9,故选取C,并与边EC一同高亮表示。 
最小生成树(prim算法)_第7张图片
顶点G是唯一剩下的顶点,它距F为11,距E为9,E最近,故高亮表示G及相应边EG。
 
最小生成树(prim算法)_第8张图片
现在,所有顶点均已被选取,图中绿色部分即为连通图的MST。在此例中,MST的权值之和为39。
 代码如下:
 
     
 
     

# include

# define max(a,b) a>b?a:b //定义max宏

const int SIZE = 1000 + 5 ;
const int INF = 0x7fffffff ; //2^31-1

int N , M , dis[SIZE] , g[SIZE][SIZE] , num[SIZE] , f[SIZE] ;
//dis[i]表示集合A中顶点到顶点i的最小权
//g[][]是邻接矩阵
//num[i]记录顶点i的边中权值最小的另一顶点
//f[]是并查集
bool visited[SIZE] ;
//visited[]标记是否在MST中

//并查集操作
int _find ( int ) ;
void _union ( int , int ) ;

int main () {
scanf ( "%d%d" , &N , &M ) ;
for ( int i=0 ; i!=N+1 ; ++i ) f[i]=i ;
for ( int i=0 ; i!=M ; ++i ) {
int u , v , cost ;
scanf ( "%d%d%d" , &u , &v , &cost ) ;
_union ( u,v ) ; //假设每条边都在MST中,看有没有可能连通
//如果有重复边,要选取更优者
g[u][v] = max(cost,g[u][v]);
g[v][u] = max(cost,g[v][u]);
}
bool liantong=true; //判断是否连通
for ( int i=1 ; i!=N && liantong ; ++i )
if ( _find(f[i])!=_find(f[i+1]) ) liantong=false; //若最终不可能在同一集合中,必定无法连通
if ( !liantong ) { printf ( "-1\n" ) ; return 0 ;}
for ( int i=0 ; i!=N+1 ; ++i ) { //初始化,选取第1个顶点作为初始顶点
dis[i] = g[1][i] ;
num[i] = 1 ;
}
int sum=0 ;
visited[1]=true ; //第1个顶点已在MST中
for ( int i=1 ; i!=N ; ++i ) {
int _max = -INF ;
int k ;
//找交叉边
for ( int j=1 ; j!=N+1 ; ++j ) {
if ( !visited[j] && dis[j]>_max ) {
_max = dis[j] ;
k = j ;
}
}
sum += g[num[k]][k] ;
visited[k]=true ; //入MST
for ( int j=1 ; j!=N+1 ; ++j ) { //优化dis[]
if ( !visited[j] && g[k][j] > dis[j] ) {
dis[j]=g[k][j] ;
num[j]=k ;
}
}
}
printf ( "%d\n" , sum ) ;
return 0 ;
}

int _find ( int i ) {
return f[i]==i ? i : ( f[i]=_find(f[i]) ) ;
}

void _union ( int i , int j ) {
int fi=_find(i);
int fj=_find(j);
if ( fi!=fj ) f[fi]=fj;
return ;
}

upd(2015,12,31):Prim算法的时间复杂度为O(|V|^2)。


你可能感兴趣的:(算法笔记,MST)