最小生成树 《啊哈算法》读书笔记

最小生成树:任何只由G的边构成,并包含G的所有顶点的树称为G的生成树(G连通). 加权无向图G的生成树的代价是该生成树的所有边的代码(权)的和. 最小代价生成树是其所有生成树中代价最小的生成树。

  假设WN=(V,{E})是一个含有n个顶点的连通网,则按照克鲁斯卡尔算法构造最小生成树的过程为:先构造一个只含n个顶点,而边集为空的子图,若将该子图中各个顶点看成是各棵树上的根结点,则它是一个含有n棵树的一个森(摘自  nocow)         


  求最小生成树的主要算法:Kruskal算法     Prim算法


Kruskal算法(克鲁斯卡尔算法):如果想要边的总长度之和最短,我们自然可以想到首先先选最短的边)将所有的边排序,从最小的边开始选,每次连通最小的边,不能形成回路,所以就要求判断两点间是否已经连通。为了优化操作,我们这里用并查集优化,判断其是否在一个树上。如果不在一个树上,就加进去,继续添加。
  时间复杂度为O(MlogM)

#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;

struct edge
{
    int u;
    int v;
    int w;
}e[10];
int n,m;
int f[7]={0},sum=0,counter=0;
void quicksort(int left,int right)
{
    int i,j;
    struct edge t;
    if(left > right)
        return;

    i = left;
    j = right;
    while(i!=j)
    {
        //注意顺序
        //先从右边找
        while(e[j].w >= e[left].w && i < j)
            j--;
        //从左边找
        while(e[i].w <= e[left].w && i < j)
            i++;
        if(i<j)
        {
            t = e[i];
            e[i]= e[j];
            e[j] = t;
            //swap(e[i],e[j]);
        }
    }
    //基准归位
    t = e[left];
    e[left]= e[i];
    e[i] = t;
    //swap(e[left],e[i]);
    quicksort(left, i-1);
    quicksort(i+1, right);
    return;
}

int getf(int v)
{
    if(f[v]==v)
        return v;
    else
    {
        //路径压缩,找到每个人的祖宗
        f[v] = getf(f[v]);
        return f[v];
    }
}

bool merge(int v,int u)
{
    int t1,t2;//t1,t2分别为v和u的boss,每次都是用boss解决
    t1=getf(v);
    t2=getf(u);
    if(t1!=t2)
    {
        f[t2] = t1;//靠左原则
        return 1;
        //路径压缩后,将f[u]的根值夜赋值为v的祖先f[t1]
    }
    return 0;
}
int main()
{
    scanf("%d%d",&n,&m);

    for(int i=1;i<=m;i++)
        scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w);

    quicksort(1,m);

    //并查集优化
    for(int i=1;i<=n;i++)
        f[i]=i;

    //Kruskal算法核心部分
    for(int i=1;i<=m;i++)//枚举
    {
        //判断一条边的两个顶点是否连通,即是否在一个集合中
        if(merge(e[i].u,e[i].v))
        {
            counter++;
            sum+=e[i].w;
        }
        if(counter == n-1)
            break;
    }
    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
*/

然后看到NOCOW上的超级精简代码,顺便贴上吧

/*使用Union-Find判断是否在一个集合,代码比较STL-style
	Author:YangZX*/
#include <iostream>
#include<algorithm>
using namespace std; const
int MAXV = 1024, MAXE = 100001;
int n, m, f[MAXV], ans, cnt;
struct edge
{
      int f, t, w;
}es[MAXE];
bool cmp(const edge &a, const edge &b)
{
     return a.w < b.w;
}
void Fill(int &a)
{
    static int cnt = 0; a = ++cnt;
}
int get(int x)
{
    return x == f[x] ? x : f[x] = get(f[x]);
}
void Kruskal(const edge &e)
{ if(get(e.f) != get(e.t))
    {
        f[get(e.f)] = get(e.t); ans += e.w; cnt++;
    }
}
void Read(edge &e)
{
     cin>>e.f>>e.t>>e.w;
}
int main()
{
    cin>>n>>m;
    for_each(es+1, es+m+1, Read);
    make_heap(es+1, es+m+1, cmp);
    sort_heap(es+1, es+m+1, cmp);
    for_each(f+1, f+n+1, Fill);
    for_each(es+1, es+m+1, Kruskal);
    cout<<(cnt < n-1 ? -1: ans)<<endl;
    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
*/


Prim算法(普里姆算法) :选中任意一个顶点,将其加入到生成树中去(这里假设为顶点1)。用数组记录生成树到各个顶点的距离,每次都是这样。从数组中选出离生成树最近的顶点加入到生成树中(这里用的dijkstra的思想),用dis数组更新为生成树到每一个不在生成树中的顶点的距离(松弛),重复直到有了n个顶点为止。

代码:

#include<iostream>
#include<cstdio>
using namespace std;

const int INF = 99999999;
int e[7][7],dis[7],book[7]={0};
int main()
{
    int n,m;
    int counter = 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;

        int t1,t2,t3;
        for(int i = 1;i <= m;i++)
        {
            scanf("%d%d%d",&t1,&t2,&t3);
            e[t1][t2] = e[t2][t1] = t3;//无向图
        }

        //初始化dis数组
        for(int i = 1;i <= n;i++)
            dis[i] = e[1][i];

        //Prim算法核心
        //将1号顶点加入生成树
        book[1] = 1;
        counter++;
        int u,v,minn;
        while(counter < n)
        {
            minn = INF;
            for(int i = 1;i <= n;i++)
            {
                if(!book[i] && dis[i] < minn)
                {
                    minn = dis[i];
                    u = i;
                }
            }

            book[u] = 1;
            counter++;
            sum += dis[u];
            for(int v = 1;v <= n;v++)
            {
                if(!book[v] && dis[v] > e[u][v])
                   dis[v] = e[u][v];
            }
        }
        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
*/


堆优化后的代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

const int INF = 99999999;
const int N = 100;
int dis[N],book[N]={0};
int h[N],pos[N],size;

void swap(int x,int y)
{
    int t;
    t = h[x];
    h[x] = h[y];
    h[y] = t;

    t = pos[h[x]];
    pos[h[x]] = pos[h[y]];
    pos[h[y]] = t;
}

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

        //如果它有左儿子,再对右儿子进行讨论
        if(i*2+1 <= size)
        {
            if(dis[h[t]] > dis[h[i*2+1]])
                t = i*2+1;
        }
        //如果发现最小的节点编号不是自己,说明子节点中有更小的
        if(t!=i)
        {
            swap(t,i);
            i = t;
        }
        else
            flag = 1;
    }
}

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;
    }
}

int pop()
{
    int t;
    t = h[1];
    pos[t] = 0;
    h[1] = h[size];
    pos[h[1]] = 1;
    size--;
    siftdown(1);
    return t;
}
int main()
{
    int n,m,k;
    int u[N],v[N],w[N],first[N],next[N];
    int counter = 0,sum = 0;
    scanf("%d%d",&n,&m);

    //读入边
    for(int i = 1;i <= m;i++)
        scanf("%d%d%d",&u[i],&v[i],&w[i]);

        //无向图
        for(int i = m+1;i <= 2*m;i++)
        {
            u[i] = v[i-m];
            v[i] = u[i-m];
            w[i] = w[i -m];
        }

        //邻接表储存边
        for(int i = 1;i <= m;i++)
            first[i] = -1;
        for(int i = 1;i <= 2*m;i++)
        {
            next[i] = first[u[i]];
            first[u[i]] = i;
        }

        //Prim算法核心
        //讲1号顶点加入生成树
        counter++;
        book[1] = 1;

        //初始化dis数组,这里是1号顶点到其余各顶点的初始距离
        dis[1] = 0;
        for(int 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(int i = 1;i <= size;i++)
            {
                h[i] = i;
                pos[i] = i;
            }
            for(int i = size/2;i >= 1;i--)
            {
                siftdown(i);
            }
            pop();//先弹出一个堆顶的元素,因为此时堆顶是1号顶点
            int j;
            while(counter < n)
            {
                j = pop();
                book[j] = 1;
                counter++;
                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]]);//对该点再堆中进行向上调整
                    }
                    k = next[k];
                }
            }
            printf("%d\n",sum);
}


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值