最小生成树——Prim详解

最小生成树

一个有 n 个结点的连通图的生成树是原图的最小连通子图,包含原图中的所有 n 个结点(意味着有n-1条边),能保持图连通并且权值和最小的边

Prim求MST

复杂度:

朴素Prim算法的时间复杂度为O(V2); 堆优化的时间复杂度为O(VlogE),V为点数,E为边数

原理:

设图G顶点集合为U,首先任意选择图G中的一点作为起始点a,将该点加入集合V,再从集合U-V中找到另一点b使得点b到V中任意一点的权值最小,此时将b点也加入集合V;现在的集合V={a,b},再从集合U-V中找到另一点c使得点c到V中任意一点的权值最小,此时将c点加入集合V。以此类推,直至所有顶点全部被加入V,此时就构建出了MST

图示:
在这里插入图片描述
由上可知,Prim算法是以点为基础进而操作每个点连接的边,每次选择权值最小的边,一步一步直到所有点都被处理过。由于Prim算法较为难懂,接下来我们一步步分析Prim算法:

假设初始时的图是这样的:
在这里插入图片描述
首先假设用邻接矩阵存图,那么设置二维数组G[ ][ ],存在的边直接输入,不存在的边设置为INF,另外需要每个点到自身的边权为0:

int G[maxn][maxn];
memset(G,0x3f,sizeof G);
for(int i=1;i<=n;i++) G[i][i]=0;

由于我们可以任选一个点作为起点,那么我们一般选择序号为1的点作为起点。设置vis[]数组判断某点是否被加入到最小生成树中;设置一个数组dis[]保存以已经生成最小生成树的点集的点为起点的边的距离,dis数组初始化均为INF。初始化过程如下:

int vis[maxn],dis[maxn];
memset(vis,0,sizeof vis);
memset(dis,0x3f,sizeof dis);

那么第一次显然我们需要将和起点直接相连的边的dis更新:

for(int i=1;i<=n;i++)
    dis[i]=G[1][i];
vis[1]=1;

这样得到了下图,红色点集代表最小生成树点集,蓝色边代表已经更新的dis[],红色边代表已经选择过的边:
在这里插入图片描述
那么接下来的步骤一个n-1次的循环就完成了,也就是每次选择到当前最小生成树点集权值最小的边,那么也就是把边另外一点加入最小生成树,接着更新dis[]数组,注意我们每次只要拿新加入的点,更新其周围的边即可,因为之前点集的点已经更新

for(int i=1;i<n;i++){  		//循环n-1次即可构造完成
	int min_len=INF,k;
	for(int j=1;j<=n;j++){
		if(!vis[j] && dis[j]<min_len){  //找到权值最小的边并记录该边不在最小生成树点
			min_len=dis[j];
			k=j;
		}
	}
	vis[k]=1;       //将上面找到的点加入最小生成树点集
	ans+=min_len;   //权值累加
	for(int j=1;j<=n;j++){  
		if(!vis[j] && dis[j]>G[k][j]) dis[j]=G[k][j];
	}
}

那么我们一步步地得到接下来的图,第一次经过循环的前半部分,选择了"1 3"这条边,并将3加入最小生成树点集:
在这里插入图片描述
第一次经过循环的后半部分,我们又更新了几条边:
在这里插入图片描述
第二次循环的前半部分,由于有两条边权值均为最小,那么我们随便选择一条边加入一个点即可:
在这里插入图片描述
第二次循环后半部分,更新和2号节点相邻的所有边:
在这里插入图片描述
第三次循环前半部分,我们选择当前边集权值最小的边"1 4"并将4加入点集:
在这里插入图片描述
至此,我们的最小生成树已经构造成功,那么后半部分也就什么都不用做了,我们得到了最小生成树:
在这里插入图片描述
这样,我们就得到朴素Prim的代码,但是下面是优化了长度,将第一次处理和后面的n-1次处理合并:

#define INF 0x7fffffff
const int N=5005; //点的最大数量
const int maxn=2e5+10; //边的最大数量
int n,m;
int G[N][N]int dis[maxn]int vis[maxn];  

void init(){
    int u,v,w;
    scanf("%d%d",&n,&m);
    memset(dis,0x3f,sizeof(dis));  
    memset(vis,0,sizeof(vis));
    memset(G,0x3f,sizeof(G));
    for(int i=1;i<=n;i++) G[i][i]=0;
    for(int i=1;i<=m;i++){
        scanf("%d%d%d",&u,&v,&w);
        if(u!=v) G[u][v]=G[v][u]=min(w,G[u][v]); //若有重边则进行此操作
    }
}

int Prim(){
    int ans=0;  
    dis[1]=0;  	//起点设置为1,那么该dis是接下来第一次一定选择的自边
    for(int i=1;i<=n;i++){  //循环n次加入n个点,构造完成
        int min_len=INF,k;
        for(int j=1;j<=n;j++){
            if(!vis[j] && dis[j]<min_len){
                min_len=dis[j];
                k=j;
            }
        }
        vis[k]=1;
        ans+=min_len;
        for(int j=1;j<=n;j++){
            if(!vis[j] && dis[j]>G[k][j]){
                dis[j]=G[k][j];
            }
        }
    }
    return ans;
}

Prim的堆优化

不难发现,上述过程和Dijkstra最短路算法思想差不多,那么类似的也有Prim的堆优化。下面我们使用效率最高的链式前向星存图,在掌握上面Prim思想和链式前向星就不难理解堆优化代码

typedef pair <int,int> P; //重定义pair对,first是权值,second是顶点号
const int N=1e5+10; //边数范围
struct node{
    int next,to,v;
}edge[N<<1];

int tot; //记录创建edge数量
int n,m; //点数和边数
int head[N],dis[N],vis[N]; 
priority_queue <P,vector<P>,greater<P> > q; //较小者先出队的优先队列

void addedge(int u,int v,int w){
    tot++;
    edge[tot].v=w;
    edge[tot].to=v;
    edge[tot].next=head[u];
    head[u]=tot;
}

void init(){ //图的初始化
    int u,v,w;
    scanf("%d%d",&n,&m);
    //全局变量初始化
    tot=0;
    memset(vis,0,sizeof(vis));
    memset(dis,0x3f,sizeof(dis));
    memset(head,0,sizeof(head));
    for(int i=1;i<=m;i++){
        scanf("%d%d%d",&u,&v,&w);
        addedge(u,v,w),addedge(v,u,w);
    }
}

int Prim(){
    int cnt=0,ans=0;  //记录最小生成树边数和权值和
    dis[1]=0;
    q.push(make_pair(0,1));
    while(!q.empty() && cnt<n){
        P u=q.top(); q.pop();
        if(vis[u.second]) continue;
        cnt++;
        ans+=u.first; 
        vis[u.second]=1;
        for(int i=head[u.second];i;i=edge[i].next)
            if(dis[edge[i].to]>edge[i].v){
                dis[edge[i].to]=edge[i].v;
                q.push(make_pair(dis[edge[i].to],edge[i].to));
            }
    }
    return ans;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值