最小生成树


前言

本篇文章将通过LeetCode例题,让读者可以通过知识点+实战的方式来领悟什么是最小生成树,最小生成树的两种算法Prim算法和Kruscal算法。


一、最小生成树是什么?

**情景引入:**假设在n个城市之间建立通信联络网,则连通n个城市只需要 n - 1 条线路。这时,自然会考虑这样一个问题,如何在最节省经费的前提下建立这个通信网。

对于n个顶点的连接网可以建立许多不同的生成树。每一棵生成树都可以是一个通信网。现在,要选择这样一棵生成树,也就是使总的耗费最少。这个问题就是构造连通网的最小代价生成树(Minimum Cost Spanning Tree)(简称为最小生成树)的问题。一棵生成树的代价就是树上各边的代价之和。

二、最小生成树算法

1、MST性质

假设N = (V, {E})是一个连通网,U是顶点集V的一个非空子集。若(u,v)是一条具有最小权值(代价)的边,其中u ∈ U, v ∈ V - U,则必存在一棵包含边(u,v)的最小生成树。

2、普里姆(Prim)算法

2.1 算法思路

假设N = (V, {E})是连通网,TE是N上最小生成树中边的集合。算法从U = {u0} (u0 ∈ V),TE = { }开始,重复执行下述操作:在所有的u ∈ U,v ∈ V - U 的边(u,v)∈ E中找一条代价最小的边
(u0, v0)并入集合TE,同时v0并入U,直至U = V 为止。此时TE中必然有n - 1条边,则T = (V, {TE})为N的最小生成树。

2.2 算法的时间复杂度

假设网中有n个顶点,则第一个进行初始化的循环语句的频度为n,第二个循环语句的频度是n-1。其中有两个是内循环(在后续给出的代码中体现):其一是求出最小值,其频度为n-1,其二是重新选择具有最小代价的边,其频度为n。由此,Prim算法的时间复杂度为O( n n n2),与网中的边数无关,因此适用于求边稠密的网的最小生成树。

3、克鲁斯卡尔(Kruscal)算法

3.1 算法思路

假设N = (V, {E})是连通网,则令最小生成树的初始状态只有n个顶点而无边的非连通图
T = (V, { }),图中每个顶点自成一个连通分量。在E中选择代价最小的边,若该边依附的顶点落在T中不同的连通分量上。在E中选择代价最小的边,若该依附的顶点落在T中不同的连通分量上,则将此边加入到T中,否则舍去此边而选择下一条代价最小的边。依次类推,直至T中所有顶点都在同一连通分量上为止。

3.2 算法的时间复杂度

算法至多对e条边各扫描一次,如果用“堆“来存放网中的边,则每次选择最小代价的边仅需O( e l o g e eloge eloge)的时间(第一次需要O( e e e))。又生成树T的每个连通分量可看成是一个等价类,则构造T加入新的边的过程类似于求等价类的过程,由此可以用并查集来描述。使得构造T的过程仅需O( e l o g e eloge eloge)的时间。由此Kruscal算法的时间复杂度为O( e l o g e eloge eloge)。

三、实战演练

1、题目编号

  1. 连接所有点的最小费用

2、题目链接

点击跳转到题目位置

3、题目描述

给你一个points 数组,表示 2D 平面上的一些点,其中 points[i] = [xi, yi] 。

连接点 [xi, yi] 和点 [xj, yj] 的费用为它们之间的 曼哈顿距离 :|xi - xj| + |yi - yj| ,其中 |val| 表示 val 的绝对值。

请你返回将所有点连接的最小总费用。只有任意两点之间 有且仅有 一条简单路径时,才认为所有点都已连接。

4、解题代码

本道题目提供两种不同的代码,分别对应上面的Prim算法和Kruscal算法。

4.1 Prim算法解题代码

// prim 算法

class Solution {
    #define maxn 1010
    #define inf -1
    int dist[maxn][maxn];
public:
    int lessthan(int a,int b){
        if(a == inf){
            return b;
        }
        if(b == inf){
            return a;
        }
        return a<b ? a:b;
    }
//定义顶点编号 0  n-1
int minSpanningTree(int n, int dist[maxn][maxn]){
    //由于是生成树,所以0一定是在树上
    //现在就是想办法吧n-1条边找出来
    //首先,找距离0这个点最近的点
    int u, dis, ret=0;
    int cost[maxn];
    memset(cost, 0, sizeof(cost));
    //0一定在最小生成树集合中
    for(int i = 0; i < n; ++i){
        cost[i]=(i==0) ? 0:dist[0][i];
    }
    //cost[i] 表示最小生成树集合和当前点的距离

    //(0,u)这条边一定是距离最小的边,这条边一定在最小生成树
    //假设这条边不在最小生成树上,并且其他n-2条边都已经求出来了
    //并且0还是个孤立点
    //那么为了让0和其他n-1个点连通
    //势必要去找一条边能让它和其他点连通
    //所以一定是找权值最小的边才是最优的

    //更新 最小生成树 和 非最小生成树 之间的距离
    while(1){
        dis = inf;
        for(int i = 0; i < n; ++i){
            if(cost[i] && lessthan(cost[i],dis)==cost[i]){
                u = i;
                dis=cost[i];
            }
        }

        if(dis == inf){
            return ret;
        }
        ret += dis;
        cost[u] = 0;
        
        for(int i = 0;i < n; ++i){
            if(cost[i] && lessthan(dist[u][i], cost[i]) == dist[u][i]){
                cost[i] = dist[u][i];            
            }
        }
    }
    return inf;
}

    int minCostConnectPoints(vector<vector<int>>& points) {
        int n = points.size();
        memset(dist, inf, sizeof(dist));
        for(int i = 0; i < n; i++){
            for(int j = 0; j < n; j++){
                dist[i][j] = dist[j][i] = abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1]);
            }
        }
    return minSpanningTree(n, dist);
    }
};

4.2 Kruscal算法解题代码

class Solution {
struct Edge {
    int start; // 顶点1
    int end;   // 顶点2
    int len;   // 长度
};
public:
    int Find(vector<int>& pre, int index){
        while(pre[index] != index){
            index = pre[index];
        }
        return index;
    }

    void Join(vector<int>& pre, int index1, int index2){
        index1 = Find(pre, index1);
        index2 = Find(pre, index2);
        if(index1 != index2){
            pre[index1] = index2;
        }
    }

    int minCostConnectPoints(vector<vector<int>>& points) {
        vector<int> pre(1010);
        int n = points.size();
        for(int i = 1; i <= n; ++i){
            pre[i] = i;
        }
        vector<Edge> edges;
        // 建立点-边式数据结构
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                Edge edge = {i, j, abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1])};
                edges.emplace_back(edge);
            }
        }
        // 按边长度排序
        sort(edges.begin(), edges.end(), [](const Edge& a, const Edge& b) {
            return a.len < b.len;
        });

        // 连通分量合并
        int num = 0;
        int ret = 0;
        for(int i = 0; i < edges.size(); ++i){
            int x = edges[i].start;
            int y = edges[i].end;
            int dis = edges[i].len;
            if(Find(pre, x) != Find(pre, y)){
                ret += dis;
                Join(pre, x, y);
            }
            if(num == n){
                break;
            }
        }
    return ret;
    }
};

5、解题思路

本道题目提供两种不同的解题思路,分别对应上面的Prim算法和Kruscal算法。

5.1 Prim算法解题思路

(1) 首先先用邻接矩阵来存储图。

(2) 接着先将点0放在选的点的集合当中。

(3) 接着更新集合中的点到不在集合中的点的最小距离,然后选取最小的边,将新的点加入集合中,依次重复上述的事情直到所有的点都在集合中。

(4) 最后返回出和即为最小生成树的权值和。

5.2 Kruscal算法解题思路

(1) 首先将所有的边按照长度从小到大排序。

(2) 因为总共有n个点,所以要选n-1个边。利用并查集来选取符合条件的n-1条长度最短的边即可。

(3) 最后返回出和即为最小生成树的权值和。

总结

通过本篇文章,相信读者已经对最小生成树有所理解,如果觉得不熟练的话,建议读者独立完成LeetCode上的这道题目,分别使用Prim算法和Kruscal算法来解决。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值