最小生成树

目录

 

一、定义

二、普里姆算法—Prim算法(适合稠密图)

三、克鲁斯卡算法—Kruskal算法(适合稀疏图)


一、定义

给定一个无向图,如果他的某个子图中,任意两个顶点都能互相连通并且是一棵树,那么这棵树就叫做生成树,如果边上有权值,那么使得边权和最小的生成树叫做最小生成树。

实际问题:我们要在n个城市中建立一个通信网络,则连通这n个城市需要布置n-1一条通信线路,这个时候我们需要考虑如何在成本最低的情况下建立这个通信网? 

 

二、普里姆算法—Prim算法(适合稠密图)

用堆优化后时间复杂度:O((n+m)logm)

【算法思想】

Prime算法是一种贪心算法,它最初将无向连通图G中所有顶点V分成两个顶点集合VA和VB。在计算过程中VA中的点为已经选好连接入生成树的点,否则属于VB。最开始的时候VA只包含任意选取的图G中的一个点u,其余的点属于VB,每次添加一个VB中的点到VA,该点是集合VB到集合VA中距离最小的一个点。直到V个顶点全部属于VA,算法结束。显然出发点不同,最小生成树的形态就不同,但边权和的最小值是唯一的。

下面我们对下面这幅图求其最小生成树:

这里写图片描述

假设我们从顶点v1开始,所以我们可以发现(v1,v3)边的权重最小,所以第一个输出的边就是:v1—v3=1: 
这里写图片描述

然后,我们要从v1和v3作为起点的边中寻找权重最小的边,首先了(v1,v3)已经访问过了,所以我们从其他边中寻找,发现(v3,v6)这条边最小,所以输出边就是:v3—-v6=4 
这里写图片描述

然后,我们要从v1、v3、v6这三个点相关联的边中寻找一条权重最小的边,我们可以发现边(v6,v4)权重最小,所以输出边就是:v6—-v4=2. 
这里写图片描述

然后,我们就从v1、v3、v6、v4这四个顶点相关联的边中寻找权重最小的边,发现边(v3,v2)的权重最小,所以输出边:v3—–v2=5 
这里写图片描述

然后,我们就从v1、v3、v6、v4,v2这2五个顶点相关联的边中寻找权重最小的边,发现边(v2,v5)的权重最小,所以输出边:v2—–v5=3 
这里写图片描述

最后,我们发现六个点都已经加入到集合U了,我们的最小生成树建立完成。

 

【算法步骤】

选定图中的任意一个顶点v0,从v0开始生成最小生成树。

(1)初始化dist[v0]=0,其他点的距离值dist[i]=∞。其中dist[i]表示集合VB中的点到VA中的点的距离值。

(2)经过N次如下步骤操作,最后得到一棵含N个顶点,N-1条边的最小生成树:

①选择一个未标记的点k,并且dist[k]的值是最小的

②标记点k进入集合VA

③以k为中间点,修改未标记点j,即VB中的点到VA的距离值

(3)得到最小生成树T。

 

【代码】

#include<bits/stdc++.h>
using namespace std;
const int maxn=105;
const int INF=0x7fffffff/2;
int vis[maxn];//判断是否标记
int p[maxn][maxn];//存图
int n,m,ans=0;
struct edge//保存边的情况,to为通往的边
{
    int to;
    int v;//权值
    friend bool operator <(const edge& x,const edge& y)
    {
        return x.v>y.v;//优先队列即最小堆
    }
}now;
priority_queue<edge>q;
void prim(int key)
{
    vis[key]=1;//标记起始点
    while(!q.empty())q.pop();
    for(int i=0;i<n-1;i++)
    {
        for(int j=0;j<n;j++)//记入新加入点的情况
        {
            if(!vis[j])//没标记过的点就加入
            {
                now.to=j;
                now.v=p[key][j];
                //printf("j=%d,p[%d][%d]=%d\n",j,key,j,p[key][j]);
                q.push(now);
            }
        }
        while(!q.empty() && vis[q.top().to])
            q.pop();
        if(q.empty())
            break;
        now=q.top();
        key=now.to;
        ans+=now.v;//累加最小边的权值
        vis[key]=1;
        q.pop();
    }
}
int main()
{
    memset(vis,0,sizeof(vis));
    //memset(p,INF,sizeof(p));
    for(int i=0;i<maxn;i++)
        for(int j=0;j<maxn;j++)
            p[i][j]=INF;
    scanf("%d%d",&n,&m);//输入n个顶点,m条边
    while(m--)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        p[u][v]=w;
        p[v][u]=w;
    }
    prim(0);
    printf("%d\n",ans);
    return 0;
}
/*
6 10
0 1 6
0 2 1
0 3 5
1 2 5
2 3 5
2 4 6
2 5 4
1 4 3
4 5 6
3 5 2
*/

 

三、克鲁斯卡算法—Kruskal算法(适合稀疏图)

用并查集优化后时间复杂度:O(mlogm+mα(n)),α(n)是一次并查集的复杂度。

【算法思想】

Kruskal算法也是一种贪心算法,它是将边按权值排序,每次从剩下的边集中选择权值最小且两个端点不在同一集合的边加入生成树中,反复操作,直到加入了n-1条边。

【算法步骤】

(1)将G中的边按权值从小到大快排。

(2)按照权值从小到大依次选边。若当前选取的边加入后使生成树T形成环,则舍弃当前边,否则标记当前边并计数。

(3)重复(2)的操作,直到生成树T中包含n-1条边,否则当遍历完所有边后,选取不到n-1条边,表示最小生成树不存在。

算法的关键在于如何判定新加入的边会不会使图G'产生环,在这里用并查集,如果新加入的边的两个端点在并查集的同一集合中,说明存在环,需要舍弃这条边,否则保留当前边,并合涉及的两个集合。

【代码】

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;
int n,m,pre[maxn],ans=0,flag=1;
struct edge
{
    int x,y;//边的两个顶点
    int z;//边的权值
}a[maxn];
bool cmp(edge a,edge b)
{
    return a.z<b.z;
}
int find1(int x)
{
    int r=x;
    while(r!=pre[r])
        r=pre[r];
    int i=x,j;
    while(i!=r)
    {
        j=pre[i];
        pre[i]=r;
        i=j;
    }
    return r;
}
void Kruskal()
{
    int f1,f2,k=0;
    for(int i=1;i<=m;i++)
    {
        f1=find1(a[i].x);
        f2=find1(a[i].y);
        if(f1!=f2)
        {
            ans+=a[i].z;
            pre[f2]=f1;//合并不相同的两个集合
            k++;
            if(k==n-1)break;
        }
    }
    if(k<n-1)
    {
        flag=0;
        return;
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        pre[i]=i;
    for(int i=1;i<=m;i++)
        scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].z);
    sort(a+1,a+m+1,cmp);
    Kruskal();
    if(flag)printf("%d\n",ans);
    return 0;
}

 

展开阅读全文

没有更多推荐了,返回首页