再谈最小生成树(Prim算法)

Prim算法用最小堆,图的邻接矩阵存储法,实现。
时间复杂度大大优化,按照鄙人的理解,其原理大概是用图的邻接矩阵存储法存储图,然后创建一个最小堆,先弄一个最小生成树,刚开始只有一个元素,即1号元素,最小堆的顶部元素到最小生成树的距离即为最小堆到最小生成树的最小距离,然后取出最顶部元素,将最顶部元素放入最小生成树中,并且以最顶部元素为踏板算出其他顶点(非树顶点)到最小生成树的距离(类似于Dijkstra算法)并进行松弛,并将最小堆恢复,并依次循环往复,直到算出n-1条边为止。
下面符两份代码(一个是Prim算法不借助堆和邻接表,另一个借助堆和邻接表,第一个时间复杂度高)

第一种:

#include <stdio.h>

int main()
{
    int n,m,min,t1,t2,t3;
    int e[7][7],book[7]={0},dis[7];
    int inf=999999;
    int cnt=0,sum=0;
    scanf("%d%d",&n,&m);
    //初始化
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            if(i==j)
                e[i][j]=0;
            else
                e[i][j]=inf;
    for(int i=0;i<m;i++)
    {
        scanf("%d%d%d",&t1,&t2,&t3);
        e[t1][t2]=t3;
        e[t2][t1]=t3;
    }
    for(int i=1;i<=n;i++)
        dis[i]=e[1][i];
    //Prim核心部分
    //将一号顶点加入生成树
    int i,j,k;
    book[1]=1;
    cnt++;
    while(cnt<n)
    {
        min=inf;
        for(i=1;i<=n;i++)
            if(book[i]==0&&dis[i]<min)
            {
                min=dis[i];
                j=i;
            }
        book[j]=1;
        sum+=dis[j];
        cnt++;
        //扫描当前定点j所有的边,在以j为中间点,更新生成树到每一个非树顶点的距离。
        for(k=1;k<=n;k++)
            if(book[k]==0&&dis[k]>e[j][k])
                dis[k]=e[j][k];
    }
    printf("%d\n",sum);
    return 0;
 }

第二种:
上面这种方法时间复杂度为O(N^2),如果借助“堆”,每次选边的时间复杂度为O(logM),然后使用邻接表来存储图,整个算法的时间复杂度降低为O(MlogN),使用堆优化,需要使用三个数组,数组dis用来记录生成树到个个顶点的距离,数组h是一个最小堆,堆里面存储的是顶点编号,但这里并不是按照顶点编号来建立最小堆的,而是按照顶点在数组dis中的值来建立最小堆的,此外还需要用一个数组pos来记录每个顶点在最小堆中的位置。
代码如下

#include <stdio.h>
#include <limits.h>

int dis[7],book[7];
int size,h[7],pos[7];

//交换函数,用来交换堆中两个元素的值
void swap(int x,int y)
{
    int t=h[x];
    h[x]=h[y];
    h[y]=t;
    //在堆中的位置也要进行交换
    t=pos[h[x]];
    pos[h[x]]=pos[h[y]];
    pos[h[y]]=t;
    return;
}
//向下调整,用于初始化堆。
void siftdown(int i)
{
    int t,flag=0;//flag用来标记是否需要继续向下调整
    while(i*2<=size&&flag==0)
    {
        if(dis[h[i]]>dis[h[i*2]])
            t=i*2;
        else
            t=i;
        if(i*2+1<=size)
            if(dis[h[i*2+1]]<dis[h[t]])
                t=i*2+1;
        //如果发现最小节点编号不是自己,则说明子节点中有比父节点更小的
        if(t!=i)
        {
            swap(t,i);
            i=t;
        }
        else
            flag=1;
    }
    return;
}
//向上调整,用于松弛之后
void siftup(int i)
{
    int flag=0;//标记用不用向上调整。
    if(i==1) return;
    while(i!=1&&flag==0)
    {
        if(dis[h[i]]<dis[h[i/2]])
            swap(i,i/2);
        else
            flag=1;
        i/=2;//更新编号i为父节点编号,以便于下一次继续向上调整
    }
    return;
}

int pop()
{
    int t;
    t=h[1];
    h[1]=h[size];
    size--;
    pos[h[1]]=1;
    siftdown(1);
    return t;
}

int main()
{
    int n,m,i,j,k;
    //u,v,w,next,数组大小要根据实际情况来制定,这里是无向图,要比2*m的值大一
    //first要比n的值大一。
    int u[19],v[19],w[19],next[19],first[19];
    int inf=INT_MAX;
    int cnt=0,sum=0;
    scanf("%d%d",&n,&m);//n表示顶点个数,m表示边的个数
    //开始读入边
    for(i=1;i<=m;i++)
        scanf("%d%d%d",&u[i],&v[i],&w[i]);
    //这里是无向图所以必须将所有的边反向存储一遍;
    for(i=m+1;i<=2*m;i++)
    {
        u[i]=v[i-m];
        v[i]=u[i-m];
        w[i]=w[i-m];
    }
    //开始使用邻接表存储边;
    for(i=1;i<=n;i++) first[i]=-1;
    for(i=1;i<=2*m;i++)
    {
        next[i]=first[u[i]];
        first[u[i]]=i;
    }
    //Prim核心;
    //将1号顶点加入生成树
    book[1]=1;
    cnt++;
    //初始化dis数组,这里是各顶点到1号顶点的距离
    dis[1]=0;
    for(i=2;i<=n;i++) dis[i]=inf;
    k=first[1];
    while(k!=-1)
    {
        dis[v[k]]=w[k];
        k=next[k];
    }
    //初始化堆
    size=n;
    for(i=1;i<=size;i++)
    {
        h[i]=i;
        pos[i]=i;
    }
    for(i=size/2;i>=1;i--)
        siftdown(i);
    pop();//弹出一个堆顶元素,因为此时堆顶元素是1while(cnt<n)
    {
        j=pop();
        book[j]=1;cnt++;sum+=dis[j];
        //扫描当前j的所有边,再以j为中间节点,进行松弛。
        k=first[j];
        while(k!=-1)
        {
            if(book[v[k]]==0&&dis[v[k]]>w[k])
            {
                dis[v[k]]=w[k];//更新距离
                siftup(pos[v[k]]);//对该点在堆中进行向上调整
                //pos[v[k]]存储的是顶点v[k]在堆中的位置
            }
            k=next[k];
        }
    }
    printf("%d\n",sum);
    return 0;
}
/*
6 9
2 4 11
3 5 13
4 6 3
5 6 4
2 3 6
4 5 7
1 2 1
3 4 9
1 3 2
*/

Kruskal算法是一步一步将森林中的树进行合并,而Prim算法则是通过每次增加一条边来建立一棵树。
Kruskal算法更适用于稀疏图,没有使用堆优化的Prim算法使用于稠密图,而使用堆优化后更加适用于稠密图。

prim(邻接矩阵+堆)

using namespace std;

//#define debug

const int N=510;

int mmp[N][N];
int size;
int h[N],pos[N];
int dis[N];
bool vis[N];

inline void myswap(int x,int y){
    swap(h[x],h[y]);
    swap(pos[h[x]],pos[h[y]]);
}

void siftdown(int i){
    int flag=0;
    while(i*2<=size&&!flag){
        int t=i;
        if(dis[h[i]]>dis[h[2*i]]) t=2*i;
        if(2*i+1<=size&&dis[h[i*2+1]]<dis[h[t]]) t=i*2+1;
        if(t!=i){
            myswap(t,i);
            i=t;
        }else flag=1;
    }
}

void siftup(int i){
    int flag=0;
    if(i==1) return ;
    while(i!=1&&!flag){
        if(dis[h[i]]<dis[h[i/2]]) myswap(i,i/2);
        else flag=1;
        i/=2;
    }
}

int pop(){
    int t=h[1];
    h[1]=h[size];
    size--;
    pos[h[1]]=1;
    siftdown(1);
    return t;
}

int main()
{
    #ifdef debug
    freopen("in.txt","r",stdin);
    freopen("out.txt","w",stdout);
    #endif // debug

    int n;
    while(scanf("%d",&n)!=EOF&&n){
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
                scanf("%d",&mmp[i][j]);
            }
        }
        for(int i=1;i<=n;i++){
            dis[i]=mmp[1][i];
        }
        //初始化堆
        size=n;
        for(int i=1;i<=size;i++){
            h[i]=i;
            pos[i]=i;
        }
        for(int i=size/2;i>=1;i--){
            siftdown(i);
        }
        mm(vis,0);
        vis[1]=1;
        int cnt=1,sum=0;
        pop();
        while(cnt<n){
            int j=pop();
            vis[j]=1;cnt++;sum+=dis[j];
            for(int i=1;i<=n;i++){
                if(!vis[i]&&dis[i]>mmp[j][i]){
                    dis[i]=mmp[j][i];
                    siftup(pos[i]);//pos[i]表示i顶点在堆中的位置。
                }
            }
        }
        printf("%d\n",sum);
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值