如何理解经典Prim算法---邻接矩阵图与双重链表图

经典Prim算法
经典的Prim算法,都是利用邻接矩阵图来实现的,我认为算法的精妙之处在于仅利用双重循环就实现了MST构造, 循环次数仅和顶点个数有关,这对于边稠密的图来说,确实是非常高效的
然而,经典Prim算法并不好理解,原因在于没有彻底搞明白一些辅助变量(数组)所表示的真正含义。

对于临时数组变量的理解
经典Prim算法,在循环构造MST的过程中,至少(或主要)依赖两个临时数组:
candidate_e[MAXVEX];
candicate_start_v[MAXVEX];
其中MAXVEX代表顶点个数。
candidate_e[]保存的是备选边的集合,candicate_e[i]的值表示边的权值,i表示该边的结束顶点。如果candicate_e[i]==0,表示顶点i已经纳入了MST;反之则表示顶点i尚未纳入MST。
备选边到底是哪些边呢?是最小生成树MST中的顶点到其它顶点的边。更严谨的说,备选边集其实是一个 最小边的集合,candidate_e[i]表示的边,是 MST中所有顶点到顶点i的边中最短的那一条,此刻candicate_start_v[i]的值正是MST中的那个顶点,也即这条最小边的起始顶点。

candicate_start_v[]保存的是备选边集candidate_e[]的起始顶点的集合,candicate_start_v[i]的值表示以i为结束顶点的边的起始顶点,candicate_start_v[i]已经纳入了MST。一句话总结,备选边<candicate_start_v[i], i>的权值是candidate_e[i]。

总的来说,构造MST的过程,就是不断的从candidate_e[]中找出最短的那条边<candidate_start_v[k], k>的过程,这个过程中还要将顶点k的某些边matrix[k][j]不断的更新至备选边集中。

对于双重循环的理解
for(i=1; i<v_num; i++)
{
unsigned int min = 0xFFFFFFFF; // 先将min设成一个比最大边还大的值
for(j=1; j<v_num; j++){
//找出备选边中最短的(并且尚未纳入MST的)一条边,min_of(candicate_e[j])
if(candidate_e[j]!=0 && candidate_e[j] < min){
min = candidate_e[j];
k = j;// 将结束顶点 j 临时赋给 k
}
}

//此时candidate_e[]的最短边是<cadicate_start_v[k], k>,边长是candidate_e[k].
candidate_e[k] = 0; //将顶点k加入MST

for(j=1; j<v_num; j++){
//检查所有以k为起点的边matrix[k][j],必要时更新至备选边集,目的是为了保证candidate_e[j]是MST到顶点 j 的边中最短的那一条。
//更新的条件是,如果边<k,j>比备选边集中的cadidate_e[j]更短,则将边<k, j>更新至备选边集
if(candidate_e[j]!=0 && matrix[k][j]<candidate_e[j]){
candidate_e[j] = matrix[k][j];
candidate_start_v[j] = k;
}
}
}
从上述双重循环看出,每一次外层循环都将决出一个新的顶点(k)纳入MST,并且将该顶点k的相邻边“有条件地“更新至备选边集。
k需要满足的条件是:
candidate_e[k]不等于0,且是candidate_e[]中最小的。candidate_e[j]等于0表示顶点j已经纳入MST,因此不能再纳一次。
将顶点k的相邻边更新至备选边集candidate_e[]需要满足的条件是:
顶点 k 到顶点 j 的边的权值matrix[k][j]比目前备选边<candidate_start_v[j], j>的权值candidate_e[j]更小。


双重链表图实现经典Prim算法--转换邻接矩阵
void Graph_MST_Prim_Classic(Graph_t* graph)
{
int min = 0;
int i = 0;
int j = 0;
int k = 0;
int t = 0;
int v_num = graph->vertex_num;
GVertex_t* pv = NULL;
GEdge_t* pe = NULL;
GEdge_t* min_e = NULL; //the min-weight edge in one outer-loop
GVertex_t* min_v = NULL; //the new vertex adding to min span tree, also the tail of min_e
int loop_cnt = 0;

unsigned int *candidate_start_v = NULL;
unsigned int* candidate_e = NULL;
unsigned int** matrix = NULL; //临时构造的邻接矩阵
candidate_start_v = (unsigned int*)malloc(sizeof(unsigned int)*v_num);
memset(candidate_start_v, 0, (sizeof(unsigned int)*v_num));
candidate_e = (unsigned int*)malloc(sizeof(int)*v_num);
memset(candidate_e, 0, (sizeof(int)*v_num));

matrix = (unsigned int**)malloc(sizeof(unsigned int*)*v_num);
memset(matrix, 0, (sizeof(unsigned int*)*v_num));
for(i=0; i<v_num; i++){
matrix[i] = (unsigned int*)malloc(sizeof(unsigned int)*v_num);
memset(matrix[i], 0xFF, (sizeof(unsigned int)*v_num));//将矩阵内所有边长默认设为无限大
}
pv = graph->top_v;
i = 0;
while(pv){
pv->already_in_mst = 0;
pv->id = i; //将所有顶点从0开始编号
i++;
pv = pv->next;
}

/* set edge value into matrix */
//遍历所有边,将边<i, j>的长度赋给矩阵matrix[i][j]
Graph_Foreach_Edge(graph, (_GForeachEdgeFunc)__edge_into_matrix, (void*)matrix);

candidate_e[0] = 0;//将顶点v0纳入MST
candidate_start_v[0] = 0;
for(i=1; i<v_num; i++){//将顶点v0的所有边纳入备选边集
candidate_e[i] = matrix[0][i]; //备选边的边长
candidate_start_v[i] = 0;//备选边的起始顶点
}
for(i=1; i<v_num; i++){
min = MAX_WEIGHT;
min_e = NULL;
j = 1;
k = 0;
for(j=1; j<v_num; j++){
loop_cnt++;
if(candidate_e[j]!=0 && candidate_e[j]<min){
min = candidate_e[j];
k = j;
}
}
printf("---------Got min edge: (%d, %d) \n", candidate_start_v[k], k);
candidate_e[k] = 0; //把顶点k添加至MST
for(j=1; j<v_num; j++){
loop_cnt++;
if(candidate_e[j]!=0 && matrix[k][j]<candidate_e[j]){
candidate_e[j] = matrix[k][j];
candidate_start_v[j] = k;
}
}
}
}

举例说明

对应的邻接矩阵如下,其中M代表无限大
===============================================================================
v0 v1 v2 v3 v4 v5 v6 v7 v8
_________________________________________________________
v0 | 0 10 M M M 11 M M M
_________________________________________________________
v1 | 10 0 18 M M M 16 M 12
_________________________________________________________
v2 | M 18 0 22 M M M M 8
_________________________________________________________
v3 | M M 22 0 20 M 24 16 21
_________________________________________________________
v4 | M M M 20 0 26 M 7 M
_________________________________________________________
v5 | 11 M M M 26 0 17 M M
_________________________________________________________
v6 | M 16 M 24 M 17 0 19 M
_________________________________________________________
v7 | M M M 16 7 M 19 0 M
_________________________________________________________
v8 | M 12 8 21 M M M M 0
_________________________________________________________
===========================================================================

开始循环之前,先将v0纳入MST,此时,
candidate_e[]: 0 10 M M M 11 M M M
candidate_start_v[]: 0 0 0 0 0 0 0 0 0

第1次外循环
查找candidate_e[]的最短边:
最短边长是candidate_e[1]=10, 对应的边是<candidate_start_v[1], 1>,也即<0, 1>;
将v1纳入MST;candidate_e[1]=0;
更新candidate_e[]:
matrix[1][]的所有边中,
matrix[1][2] < candidate_e[2],更新至备选边
matrix[1][6] < candidate_e[6],更新至备选边
matrix[1][8] < candidate_e[8],更新至备选边

此时,
candidate_e[]: 0 0 18 M M 11 16 M 12
candidate_start_v[]: 0 0 1 0 0 0 1 0 1

第2次外循环
查找candidate_e[]的最短边:
最短边长是candidate_e[5]=11,对应的边是<candidate_start_v[5], 5>,也即<0, 5>;
因此将v5纳入MST,candidate_e[5]=0;
更新candidate_e[]:
matrix[5][]的所有边中,
matrix[5][4]<candidate_e[4],更新备选边

此时,
candidate_e[]: 0 0 18 M 26 0 16 M 12
candidate_start_v[]: 0 0 1 0 5 0 1 0 1

第3次外循环
查找candidate_e[]的最短边:
最短边长是candidate_e[8]=12,对应的边是<candidate_start_v[8], 8>,也即<1, 8>;
因此将v8纳入MST,candidate_e[8]=0;
更新candidate_e[]:
matrix[8][]的所有边中,
matrix[8][2]<candidate_e[2],更新备选边
matrix[8][3]<candidate_e[3],更新备选边

此时,
candidate_e[]: 0 0 8 21 26 0 16 M 0
candidate_start_v[]: 0 0 8 8 5 0 1 0 1

... ...
最终,依次生成的MST为:
v0----(10)----v1
v0----(11)----v5
v1----(12)----v8
v8----(8)----v2
v1----(16)----v6
v6----(19)----v7
v7----(7)----v4
v7----(16)----v3

进一步优化:比邻接矩阵更快!
实际情况下,某顶点并不是和所有其它顶点都有边,这也是我们为何倾向于用链表来存储边。
因此, 可优化之处在于内层循环的第二步,遍历新加入MST的顶点K的所有边的方法,由数组遍历改成链表遍历,这样便减少了不必要的循环。

void Graph_MST_Prim_LinkedList(Graph_t* graph)
{
int min = 0;
int i = 0;
int j = 0;
int k = 0;
int t = 0;
int v_num = graph->vertex_num;
GVertex_t* pv = NULL;
GEdge_t* pe = NULL;
GEdge_t* min_e = NULL; //the min-weight edge in one outer-loop
int loop_cnt = 0;

GVertex_t** v_array = NULL; //顶点指针的反向映射:已知顶点id,获取顶点指针
unsigned int *candidate_start_v = NULL;
unsigned int* candidate_e = NULL;
v_array = (GVertex_t**)malloc(sizeof(GVertex_t*)*v_num);
memset(v_array, 0, (sizeof(GVertex_t*)*v_num));
candidate_start_v = (unsigned int*)malloc(sizeof(unsigned int)*v_num);
memset(candidate_start_v, 0, (sizeof(unsigned int)*v_num));
candidate_e = (unsigned int*)malloc(sizeof(int)*v_num);
memset(candidate_e, 0, (sizeof(int)*v_num));

pv = graph->top_v;
i = 0;
while(pv){
pv->already_in_mst = 0;
v_array[i] = pv;//对顶点指针数组赋值
pv->id = i;//对顶点从0开始编号
i++;
pv = pv->next;
}
candidate_e[0] = 0;//将顶点v0纳入MST
for(i=0; i<v_num; i++){
candidate_e[i] = MAX_WEIGHT;//将所有备选边长度设为无限大
candidate_start_v[i] = 0;//将所有备选边的起始顶点设为v0
}
//遍历v0的所有边,将v0所有边放入备选边集
pe = v_array[0]->first_edge;
while(pe){
candidate_e[pe->tail->id] = pe->length;
pe = pe->next;
}
for(i=1; i<v_num; i++){
min = MAX_WEIGHT;
min_e = NULL;
j = 1;
k = 0;
for(j=1; j<v_num; j++){
loop_cnt++;
if(candidate_e[j]!=0 && candidate_e[j]<min){
min = candidate_e[j];
k = j;
}
}
printf("\n");
printf("---------Got min edge: (%d, %d) \n", candidate_start_v[k], k);
candidate_e[k] = 0;
printf("add k=%d into MST\n", k);

pe = v_array[k]->first_edge;
while(pe){//遍历以顶点k为起点的边链表
loop_cnt++;
//pe->tail表示的是这条边的结束顶点
if(candidate_e[pe->tail->id]!=0 && pe->length < candidate_e[pe->tail->id]){
candidate_e[pe->tail->id] = pe->length;
candidate_start_v[pe->tail->id] = k;
}
pe = pe->next;
}
}

printf("loop times: %d\n", loop_cnt);

/*
free the memory
*/
}

如此,便在不借助邻接矩阵的情况下,实现了时间复杂度为O(N^2)的Prim算法,并且理论上来说, 本算法的循环次数必然会比邻接矩阵式的循环次数更少,因为此处内层第二个循环while(pe){...}肯定要比邻接矩阵的for(j=1;j>v_num;j++){...}的循环次数要少。
loop_cnt变量记录了循环次数,也实际上验证了这一点。
这样一来,对于边稀疏的图,运算效率上会有非常显著的提升!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值