最小生成树概念、最小生成树边权之和————附模版伪代码、实现代码、示例

1 概念

1.1 定义

最小生成树:在一个给定的无向图G(V,E)中求一颗树T,使这棵树拥有图G中的所有顶点,且所有边都来自于图G中的边,并且满足整颗树的边权之和最小。

1.2 性质

  • 1 最小生成树是树,因此其边数等于其顶点树减一,且树内一定不会有环;
  • 2 对于给定的图G(V,E),其最小生成树可以不唯一,但其边权之和一定唯一;
  • 3 由于最小生成树是无向图上生成的,因此其根结点可以是这颗树上的任意结点。

2 求解最小生成树

2.1 prim算法(普里姆算法)

2.1.1 算法思想

  • 对于图G(V,E)设置集合S来存放已被访问的顶点,然后执行n次下面的两个步骤:
    • 1 每次从集合V-S中选择与集合S最近的一个顶点(记为u),访问u并将其加入集合S,同时把这条离集合S最近的边加入最小生成树中,
    • 2 令顶点u作为集合S和集合V-S连接的接口,优化从u能到达的未访问的顶点v与集合S的最短距离

2.1.2 模版伪代码

//图G一般设置为全局变量;数组d为顶点与集合S的最短距离
Prim(G, d[]){
    初始化;
    for (循环n次)
    {
        u = 使d[u]最小的海未被访问的顶点标号;
        记u已被访问;
        for (从u出发能到达的所有顶点v)
        {
            if(v未被访问&&以u为中介点使得v与集合S的最短距离d[v]更优){
                将G[u][v]赋值给v与集合S的最短距离d[v];
            }
        }
    }
}

2.1.3 代码

2.1.3.1 邻接矩阵
const int MAXV = 1000;
const int INF = 0x3fffffff;
int G[MAXV][MAXV];//图
int n;//顶点个数
bool vis[MAXV] = {false};//记录顶点是否被访问
int d[MAXV];//顶点与集合S的最短距离

int prim(){//默认0号为初始点,函数返回最小生成树的边权之和
    fill(d, d + MAXV, INF);
    d[0] = 0;//只有0号顶点到集合S的距离为0,其余为INF
    int ans = 0;//存放最小生成树的边权之和
    for (int i = 0; i < n; ++i)
    {
        int u = -1, min = INF;
        for (int j = 0; j < n; ++j)
        {
            if(vis[j] == false && d[j] < min){
                u = j;
                min = d[j];
            }
        }
        //找不到小于INF的d[u],则剩下的顶点和集合S不连通
         if(u == -1) return -1;
         vis[u] = true;
         ans += d[u];//将与集合S距离最小的边加入最小生成树
    
        for (int v = 0; v < n; ++v)
        {
            //v未访问 && u能到达v && 以u为中介点可以是v离集合S更近
            if(vis[v] == false && G[u][v] != INF && G[u][v] < d[v]){
                d[v] = G[u][v];
            }
        }
    }

    return ans;
}
2.1.3.2 邻接表
struct node
{
    int v;
    int dis;
};

const int MAXV = 1000;
const int INF = 0x3fffffff;
vector<node> G[MAXV];//图
int n;//顶点个数
bool vis[MAXV] = {false};//记录顶点是否被访问
int d[MAXV];//顶点与集合S的最短距离

int prim(){//默认0号为初始点,函数返回最小生成树的边权之和
    fill(d, d + MAXV, INF);
    d[0] = 0;//只有0号顶点到集合S的距离为0,其余为INF
    int ans = 0;//存放最小生成树的边权之和
    for (int i = 0; i < n; ++i)
    {
        int u = -1, min = INF;
        for (int j = 0; j < n; ++j)
        {
            if(vis[j] == false && d[j] < min){
                u = j;
                min = d[j];
            }
        }
        //找不到小于INF的d[u],则剩下的顶点和集合S不连通
         if(u == -1) return -1;
         vis[u] = true;
         ans += d[u];//将与集合S距离最小的边加入最小生成树
    
        for (int j = 0; j < G[u].size(); ++j)
        {
            int dis = G[u][j].dis;
            int v = G[u][j].v;
            //v未访问  && 以u为中介点可以是v离集合S更近
            if(vis[v] == false && dis < d[v]){
                d[v] = dis;
            }
        }
    }

    return ans;
}

2.1.4 注意点

  • 上面写法的算法时间复杂度为O(V2);
  • 邻接表的写法可以通过堆优化,将时间复杂度降到O(VlogV+E)
  • 尽量在顶点较少边较多的情况下(稠密图),使用prim算法

2.1.5 示例

输入:
6 10 //6个顶点,10条边。以下10行为10条边
0 1 4 //边0->1和1->0的边权为4,下同
0 4 1
0 5 2
1 2 6
1 5 3
2 3 6
2 5 5
3 4 4
3 5 5
4 5 3
输出:
15 //最小生成树边权

#include <cstdio>
#include <algorithm>
#include <vector>

using std::vector;
using std::fill;

struct node
{
    int v;
    int dis;
    node(int _v, int _dis): v(_v), dis(_dis){}
};

const int MAXV = 1000;
const int INF = 0x3fffffff;
vector<node> G[MAXV];//图
int n;//顶点个数
bool vis[MAXV] = {false};//记录顶点是否被访问
int d[MAXV];//顶点与集合S的最短距离

int prim(){//默认0号为初始点,函数返回最小生成树的边权之和
    fill(d, d + MAXV, INF);
    d[0] = 0;//只有0号顶点到集合S的距离为0,其余为INF
    int ans = 0;//存放最小生成树的边权之和
    for (int i = 0; i < n; ++i)
    {
        int u = -1, min = INF;
        for (int j = 0; j < n; ++j)
        {
            if(vis[j] == false && d[j] < min){
                u = j;
                min = d[j];
            }
        }
        //找不到小于INF的d[u],则剩下的顶点和集合S不连通
        if(u == -1) return -1;
        vis[u] = true;
        ans += d[u];//将与集合S距离最小的边加入最小生成树

        for (int j = 0; j < G[u].size(); ++j)
        {
            int dis = G[u][j].dis;
            int v = G[u][j].v;
            //v未访问  && 以u为中介点可以是v离集合S更近
            if(vis[v] == false && dis < d[v]){
                d[v] = dis;
            }
        }
    }
    return ans;
}

int main(int argc, char const *argv[])
{
    int m, u, v, w;
    scanf("%d%d", &n, &m);
    for (int i = 0; i < m; ++i)
    {
        scanf("%d%d%d", &u, &v, &w);
        G[u].push_back(node(v,w));
        G[v].push_back(node(u,w));
    }

    int ans = prim();
    printf("%d\n", ans);
    return 0;
}
/*
6 10
0 1 4
0 4 1
0 5 2
1 2 6
1 5 3
2 3 6
2 5 5
3 4 4
3 5 5
4 5 3
*/

2.2 kruskal算法(克鲁斯卡尔算法)

2.2.1基本思想

  • 在初始状态时,隐去图中所有边,这样图中每个顶点都自成一个连通块,。之后执行下面的步骤:
    • 1 对所有边按边权按从小到大排序;
    • 2 按边权从小到大测试所有边,如果当前测试边所连接的两个顶点不在同一个连通块中,则把这条测试边加入当前最小生成树中;否则,将边舍弃;
    • 3 执行步骤2, 直到最小生成树中的边数等于顶点树减1或是测试完所有边时结束。而当结束时,如果最小生成树的边数小于总顶点树减1,说明该图不连通。
  • 简单说:每次选择图中最小边权的边,如果边两端的顶点在不同连通块中,则把这条边加入最小生成树中,

2.2.2 模版伪代码

  • 1 边的定义
    因为需要判断边的两个断点是否在不同的连通块中,因此边的两个端点编号一定需要;算法涉及边权,因此边权也需要。
struct edge
{
    int u, v;//边的两个端点编号
    int cost;//边权
};
  • 2 排序函数
    让数组E按边权从小到大排序
bool cmp(edge a, edge b){
    return a.cost < b.cost; 
}
  • 3 伪代码
int krukal(){
    令最小生成树的边权之和为ans、最小生成树的当前边数为Num_Edge;
    将所有边按边权大小排序;
    for(从小到大枚举所有的边){
        if(当测试的两个端点在不同连通块中){
            将测试边加入最小生成树中;
            ans += 测试边的边权;
            最小生成树的当前边权Num_Edge加1;
            当边数Num_Edge等于顶点数减1时结束循环;
        }
    }
    return ans;
}

2.2.3 实现代码

const int MAXV = 120;//最多顶点数
const int MAXE = 1000;//最多边数
int father[MAXV];//并查集数组

struct edge
{
    int u, v;//边的两个端点编号
    int cost;//边权
}E[MAXE];



int findFather(int x){
    int a = x;
    while(x != father[x]){
        x = father[x];
    }

    while(a != father[a]){
        int z = a;
        a = father[a];
        father[z] = x;
    }

    return x;
}


bool cmp(edge a, edge b){
    return a.cost < b.cost; 
}

int krukal(int n, int m){
    int ans = 0, Num_Edge = 0;//ans:所求边权之和;Num_Edge:当前生成树的边数
    for (int i = 1; i <= n; ++i)//假设顶点范围为1~n
    {
        father[i] = i;//并查集初始化
    }
    sort(E, E + m, cmp);//所有边权从小到大排序

    for (int i = 0; i < m; ++i)//枚举所有边
    {
        int faU = findFather(E[i].u); //查询测试边的两个端点所在集合的根结点
        int faV = findFather(E[i].v);
        if(faU != faV){//如果不在一个集合中
            father[faU] = faV;//合并集合(把测试边加入最小生成树)
            ans += E[i].cost;//边权之和加入测试边的边权
            Num_Edge++;//当前生成树边数加1
            if(Num_Edge == n - 1){//边数等于顶点树-1时,结束算法
                break;
            }
        }
    }
        if(Num_Edge != n - 1){//无法连接时,返回-1
            return -1;
        }else{//返回最小生成树的边权之和
            return ans;
        }
}

2.2.4 注意

*时间复杂度主要在于对边进行排序,时间复杂度为O(ElogE)

  • 适合顶点较多,边数较少的情况(稀疏图)

2.2.5 示例

输入:
6 10 //6个顶点、10条边,下面跟着10条无向边
0 1 4 //0号顶点与1号顶点的无向边的边权为4
0 4 1
0 5 2
1 2 1
1 5 3
2 3 6
2 5 5
3 4 5
3 5 4
4 5 3
输出:
11 //最小生成树的边权之和

#include <cstdio>
#include <algorithm>

using std::sort;

const int MAXV = 110;
const int MAXE = 10000;
int father[MAXV];

struct edge
{
    int u, v;
    int cost;
}E[MAXE];

bool cmp(edge a, edge b){
    return a.cost < b.cost;
}

int findFather(int x){
    int a = x;
    while(x != father[x]){
        x = father[x];
    }

    while(a != father[a]){
        int z = a;
        a = father[a];
        father[z] = x;
    }

    return x;
}


int kruskal(int n, int m){
    int ans = 0, Num_Edge = 0;
    for (int i = 0; i < n; ++i)
    {
        father[i] = i;
    }

    sort(E, E + m, cmp);
    for (int i = 0; i < m; ++i)
    {
        int faU = findFather(E[i].u);
        int faV = findFather(E[i].v);
        if(faU != faV){
            father[faU] = faV;
            ans += E[i].cost;
            Num_Edge++;
            if(Num_Edge == n - 1){
                break;
            }
        }
    }
    if(Num_Edge != n - 1){
        return -1;
    }else {
        return ans;
    }
}

int main(int argc, char const *argv[])
{
    int n, m;
    scanf("%d%d", &n, &m);
    for (int i = 0; i < m; ++i)
    {
        scanf("%d%d%d", &E[i].u, &E[i].v, &E[i].cost);
    }
    int ans = kruskal(n, m);
    printf("%d\n", ans);
    return 0;
}

/*
6 10
0 1 4
0 4 1
0 5 2
1 2 1
1 5 3
2 3 6
2 5 5
3 4 5
3 5 4
4 5 3
*/
发布了378 篇原创文章 · 获赞 52 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qq_33375598/article/details/104427436