最小生成树(MST):Prim / Kruskal

来源:互联网 发布:网络销售平台搭建方案 编辑:程序博客网 时间:2024/06/11 17:13

* 假设T1集合是已加入最小生成树中的点,T2集合是剩下的待加入T2的点 
* 我们要做的是把T2集合中离T1最近的那个点,加入T1 
* 所以我们需要知道: 
* 集合T2内各顶点到集合T1的距离 
*  
* 为此,我们用两个数组: 
* cost[i]:用来表示T2中点i到T1的距离; 
* pre[i]:用来表示T2中点i和T1中哪个点最近(为了输出加入时的路径,不输出路径则不需要此数组) 
* visited[i],代表i加入了T1集合  
*  
* NO.1 
* 选取起始点v,将v加入T1,并对剩余各点进行距离初始化: initCost() 
* NO.2   
* 选择一个在T2集合中,距离T1集合最近的点k: findNext() 
* NO.3 
* 将k加入到集合T1中:visited[k] = 1
* NO.4   
* 更新剩余T2中的各点到T1集合的距离 
* (如果因为新加入的点k而使得T2中的点到T1的距离缩小,将此点的前导点改为k) 
*if     edge[k][i]<cost[i] 
*then   cost[i]=edge[k][i];  pre[i]=k; 

* 这就完成了一次操作,加入一条边。 
* 由于MST一定是n-1条边,进行n-1次后就完成了MST。

#include<stdio.h>#include<string.h>#define INF 0xfffffff// infinity#define MAXSIZE 20int num, n;                                 // num: line; n: vertexint edge[MAXSIZE][MAXSIZE];                 // 邻接矩阵int cost[MAXSIZE], pre[MAXSIZE], visited[MAXSIZE];     // 距离标记;前导点标记                                            // 所有数组从1开始void Prim(int u);int main(){    int i, j;    int row, col, value;        memset(edge, 0, sizeof(edge));//void * memset (void * p,int c,size_t n);        scanf("%d%d", &n, &num);        for(i = 0; i < num; i++)// 构造邻接矩阵    {        scanf("%d%d%d", &row, &col, &value);        edge[row][col] = edge[col][row] = value;    }        for(i = 1; i <= n; i++)    //将邻接矩阵对角线赋为0        for(j = 1; j <= n; j++)//原来就为0的赋为INF            if(i == j)                edge[i][j] = 0;            else if( edge[i][j] == 0 )                edge[i][j] = INF;                        Prim(1);    //Prim算法,不妨从顶点1开始                        return 0;}//NO.1 对距离进行初始化void initCost(int u){    int i;    for(i = 1; i <= n; i++)    {        cost[i] = edge[u][i];        pre[i] = u;    }    cost[u] = 0;    visited[u] = 1;}//NO.2 找到不在T1中,边的权值最小的那个点int findNext(){    int i;    int mincost = INF, v = -1;    for(i = 1; i <= n; i++)    {        if(visited[i] != 1 && cost[i] < mincost)        {            mincost = cost[i];            v = i;        }    }    return v;}//NO.4 加入后,对距离进行更新void updateCost(int v){    int i;    for(i = 1; i <= n; i++)    {        if(visited[i] != 1 && edge[v][i] < cost[i])        {            cost[i] = edge[v][i];            pre[i] = v;        }    }}//在dijkstra算法中逆向打印点v到源点的路径//在prim算法中不需要void print_path(int v) //打印最短路的路径(反向){    while(v != pre[v]) //前驱    {            printf("%d<--", v);        v = pre[v];    }    printf("%d\n", v);}void Prim(int u){    int i, j;    int v = -1;    int weight = 0;        initCost(u);                            //NO.1 对距离进行初始化        for(i = 1; i < n; i++)//循环n-1次,加入n-1个顶点    {        v = findNext();             //NO.2 找到不在T1中,边的权值最小的那个点        if(v != -1)    //如果找到那个点v        {            printf("%d--(%d)-->%d\n",                 pre[v], cost[v], v);     //打印加入的流程            visited[v] = 1;    //NO.3 将v点加入T1             weight += cost[v];                        updateCost(v);//NO.4 对距离进行更新        }     }    printf("The sum-weight of the MST is %d\n", weight);    //just for fun    print_path(7);                          //比如打印点7到源点的逆向路径玩玩儿=.=}





相对于dijkstra而言,Prim最小生成树和Dijkstra最短路径的微小的差别为:“权值最低”的定义不同!

Prim的“权值最低”是相对于U中的任意一点而言的,也就是把U中的点看成一个整体,每次寻找V - U中跟U的距离最小(也就是跟U中任意一点的距离最小)的一点加入U;而Dijkstra的“权值最低”是相对于v0而言的,也就是每次寻找V-U中跟v0的距离最小的一点加入U

因此对于最短路径,只要将上面MST的路径距离更新的代码改成以下部分(多加了lowcost[v]):

if(visited[i] != -1 && edge[v][i] + cost[v] < cost[i]){    cost[i] = edge[v][i] + cost[v];    pre[i] = v;}

就OK了!


相对于Prim的复杂度O(n*n),用Kruskal是个更简单也更好的选择,它的复杂度完全由排序决定。如果是用快排,则Kruskal的复杂度可以简化为O(n*logn)。

//Kruskal#include<stdio.h>#include<stdlib.h>#define N 1010typedef struct node{    int begin, end;                                 //这里的begin和end其实并没有“有向图”的概念,还是代表无向图    int value;}Node;Node edge[N];                                       //存储边的信息int root[N];                                        //存储每个点所在的集合的根int rising(const void *a, const void *b){    return ((Node *)a)->value - ((Node *)b)->value; //注意强制转型的优先级没有->高}int find(int point)                             //并查集的“查”{    if(root[point] == -1)                       //如果根结点信息为-1,则自己就是自己所在集合的根        return point;                                   //所以返回自己    return root[point] = find(root[point]);     //否则压缩路径,并返回point所在集合的根=.=话说这一句信息量好大}//话说刚刚的return一次性将压缩路径和返回集合所在的根都解决了,相当于下面这个函数/*int pre[1000 ];int find(int x)                  //查找根节点{     int r=x;    while ( pre[r ] != r )       //找到根节点 r          r=pre[r ];     int i=x , j ;    while( i != r )              //路径压缩    {         j = pre[ i ];           // 在改变上级之前用临时变量  j 记录下他的值          pre[ i ]= r ;           //把上级改为根节点         i=j;    }    return r ;}*/void join(int x, int y)                         //并查集的“并”{    if( find(x) != find(y) )         //如果两个点xy所在集合的根不一样,        root[ find(x) ] = find(y);   //则将一个点合并到另一个集合(实际操作是置两个点的根为同一个根)}int main(){    int n, m, i, sum = 0;    for(i=0; i<N; i++)                          //刚开始将所有的点的根初始化为-1        root[i] = -1;    scanf("%d%d", &n, &m);    for(i=0; i<m; i++)        scanf("%d%d%d", &edge[i].begin, &edge[i].end, &edge[i].value);    qsort(edge, m, sizeof(Node), rising);       //按照边的权值快排nlogn    for(i=0; i<m; i++)    {        if( find(edge[i].begin) != find(edge[i].end) )//如果两个点不在一个集合,意味着连接两点不会成环        {            join(edge[i].begin, edge[i].end);           //将一个点并到另一个点所在的集合            sum += edge[i].value;        }    }    printf("%d", sum);    return 0;}



0 0
原创粉丝点击