【算法】关于图论中的最小生成树(Minimum Spanning Tree)详解

喜欢的话可以扫码关注我们的公众号哦,更多精彩尽在微信公众号【程序猿声】

本节纲要

  • 什么是图(network)
  • 什么是最小生成树 (minimum spanning tree)
  • 最小生成树的算法

什么是图(network)?

这里的图当然不是我们日常说的图片或者地图。通常情况下,我们把图看成是一种由“顶点”和“边”组成的抽象网络。在各个“顶点“间可以由”边“连接起来,使两个顶点间相互关联起来。图的结构可以描述多种复杂的数据对象,应用较为广泛,看下图:

为了更好地说明问题,下面我们看一个比较老套的通信问题:

在各大城市中建设通信网络,如下图所示,每个圆圈代表一座城市,而边上的数字代表了建立通信连接的价格。那么,请问怎样才能以最小的价格使各大城市能直接或者间接地连接起来呢?

我们需要注意两点:

  • 最小的价格
  • 各大城市可以是直接或者间接相连的

稍稍留心可以发现,题目的要求是,城市只需要直接或者间接相连,因此,为了节省成本,我们稍稍优化一下上述方案如下:

可以看到,我们砍掉了原先在AD,BE之间的两条道路,建设价格自然就降下来了。当然这个方案也是符合我们题目的要求的。按照国际惯例,这里要说蛋是了。上面的实例由于数据很简单,优化的方案很easy就看出来了。但在实际中,数据量往往是非常庞大的。所以,我们更倾向于设计一种方法,然后利用计算机强大的运算能力帮我们处理这些数据得出最优的方案。
那么,针对上述问题,我们一起来看看如何应用图的相关知识来实现吧。

什么是最小生成树(minimum spanning tree)

为了直观,还是用图片给大家解释一下:

  • 对于一个图而言,它可以生成很多树,如右侧图2,图3就是由图1生成的。
  • 从上面可以看出生成树是将原图的全部顶点以最少的边连通的子图,对于有n个顶点的连通图,生成树有n-1条边,若边数小于此数就不可能将各顶点连通,如果边的数量多于n-1条边,必定会产生回路。
  • 对于一个带权连通图,生成树不同,树中各边上权值总和也不同,权值总和最小的生成树则称为图的最小生成树。

关于最小生成树的算法(Prim算法和Kruskal算法)

Prim算法

基本思想:
假设有一个无向带权图G=(V,E),它的最小生成树为MinTree=(V,T),其中V为顶点集合,T为边的集合。求边的集合T的步骤如下:

①令 U={u0},T={}。其中U为最小生成树的顶点集合,开始时U中只含有顶点u0(u0可以为集合V中任意一项),在开始构造最小生成树时我们从u0出发。

②对所有u∈U,v∈(V – U)(其中u,v表示顶点)的边(u,v)中,找一条权值最小的边(u’,v’),将这条边加入到集合T中,将顶点v’加入集合U中。

③直到将V中所有顶点加入U中,则算法结束,否则一直重复以上两步。

④符号说明:我们用大写字母表示集合,用小写字母表示顶点元素,用<>表示两点之间的边。

为了更好的说明问题,我们下面一步一步来为大家展示这个过程。

  1. 初始状态:U={a} V={b,c,d,e } T={}
  2. 集合U和V相关联的权值最小的边是<a,b style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">,于是我们将b加入U。U={a,b},V={d,c,e },T={<a,b style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">}
  3. 此时集合U和V相关联的权值最小的边是<b,c style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">,于是我们将c加入U。U={a,b,c} ,V={d,e },T={<a,b style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">, <b,c style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">}
  4. 显然此时集合U和V中相关联的权值最小的边是<c,d style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">,于是我们将d加入U。U={a,b,c,d} ,V={e },T={<a,b style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">, <b,c style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">,<c,d style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">}
  5. 最后集合U和V中相关联的权值最小的边是<d,e style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">,于是将e加入U。U={a,b,c,d,e} ,V={},T={<a,b style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">, <b,c style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">,<c,d style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">,<d,e style="font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;">}。

    到此所有点访问完毕。

代码实现

 1//prime算法 2//将城市X标记为visit=true时,就表示该城市加入到集合U,用sum累加记录边的总费用 3 4#include<iostream> 5#define NO 99999999   //99999999代表两点之间不可达 6#define N 5 7using namespace std; 8 9bool visit[N];10long long money[N] = { 0 };11long long graph[N][N] = {0};1213void initgraph()14{15    for (int i = 0; i < N; i++)16    {17        for (int j = 0; j < N; j++)18        {19            scanf(" %lld", &graph[i][j]);20        }21    }2223}2425void printgraph()26{27    for (int i = 0; i < N; i++)28    {29        for (int j = 0; j < N; j++)30        {31            printf(" %lld", graph[i][j]);32        }33    }3435}3637int prim(int city)38{39    initgraph();40    printgraph();41    int index = city;42    int sum = 0;43    int i = 0;44    int j = 0;45    cout <<"访问节点:" <<index << "\n";46    memset(visit, false, sizeof(visit));47    visit[city] = true;48    for (i = 0; i < N; i++)49    {50        money[i] = graph[city][i];//初始化,每个与城市city间相连的费用存入money,以便后续比较51    }5253    for (i = 1; i < N; i++)54    {55        int minor = NO;56        for (j = 0; j < N; j++)57        {58            if ((visit[j] == false) && money[j] < minor)  //找到未访问的城市中,与当前最小生成树中的城市间费用最小的城市59            {60                minor = money[j];61                index = j;62            }63        }64        visit[index] = true;65        cout << "访问节点:" << index << "\n";66        sum += minor; //求总的最低费用67        /*这里是一个更新,如果未访问城市与当前城市间的费用更低,就更新money,保存更低的费用*/68        for (j = 0; j < N; j++)69        {70            if ((visit[j] == false) && money[j]>graph[index][j])71            {72                money[j] = graph[index][j];73            }74        }75    }76    cout << endl;77    return sum;               //返回总费用最小值78}79int main()80{81    cout << "修路最低总费用为:"<< prim(0) << endl;//从城市0开始82    return 0;83}

Kruskal算法

解最小生成树的另一种常见的算法是Kruskal算法,它比Prim算法更直观。
Kruskal算法的做法是:每次都从剩余边中选取权值最小的,当然,这条边不能使已有的边产生回路。手动求解会发现Kruskal算法异常简单,下面是一个例子

先对边的权值排个序:
1(V0,V4)、2(V2,V6)、4(V1,V3)、6(V1,V2)、8(V3,V6)、10(V5,V6)、12(V3,V5)、15(V4,V5)、20(V0,V1)

首选边1(V0,V4)、2(V2,V6)、4(V1,V3)、6(V1,V2),此时的图是这样

显然,若选取边8(V3,V6)则会出现环,则必须抛弃8(V3,V6),选择下一条10(V5,V6)没有问题,此时图变成这样

显然,12(V3,V5)同样不可取,选取15(V4,V5),边数已达到要求,算法结束。最终的图是这样的


算法逻辑很容易理解,但用代码判断当前边是否会引起环的出现则很棘手。这里简单提一提连通分量

  • 在无向图中,如果从顶点vi到顶点vj有路径,则称vi和vj连通。如果图中任意两个顶点之间都连通,则称该图为连通图,否则,将其中较大的连通子图称为连通分量。
  • 在有向图中,如果对于每一对顶点vi和vj,从vi到vj和从vj到vi都有路径,则称该图为强连通图;否则,将其中的极大连通子图称为强连通分量。

算法说明
为了判断环的出现,我们换个角度来理解Kruskal算法的做法:初始时,把图中的n个顶点看成是独立的n个连通分量,从树的角度看,也是n个根节点。我们选边的标准是这样的:若边上的两个顶点从属于两个不同的连通分量,则此边可取,否则考察下一条权值最小的边。
于是问题又来了,如何判断两个顶点是否属于同一个连通分量呢?这个可以参照并查集的做法解决。它的思路是:如果两个顶点的根节点是一样的,则显然是属于同一个连通分量。这也同样暗示着:在加入新边时,要更新父节点。

 1//kruskal算法 2 3#include<cstdio> 4#include<iostream> 5#include<cstring> 6#include<cstdlib> 7#include<algorithm> 8#include<cmath> 9#include<map>10#include<set>11#include<list>12#include<vector>13using namespace std;14#define N 1000515#define M 5000516#define qm 10000517#define INF 214748364718struct arr{19    int ff, tt, ww;20}c[M << 1];// 存储边的集合,ff,tt,ww为一条从ff连接到tt的权值为ww的边 21int tot = 0;//边的总数 22int ans = 0;//最小生成树的权值和 23int f[N];//并查集 24bool comp(const arr & a, const arr & b){25    return a.ww < b.ww;26}27int m, n;//边数量,点数量 28int getfa(int x){29    return f[x] == x ? x : f[x] = getfa(f[x]);30}//并查集,带路径压缩 3132inline void add(int x, int y, int z){33    c[++tot].ff = x;34    c[tot].tt = y;35    c[tot].ww = z;36    return;37}//新增一条边 3839void kruscal(){40    for (int i = 1; i <= n; i ++) f[i] = i;41    for (int i = 1; i <= m; i ++){42        int fx = getfa(c[i].ff);//寻找祖先 43        int fy = getfa(c[i].tt);44        if (fx != fy){//不在一个集合,合并加入一条边 45            f[fx] = fy;46            ans += c[i].ww;47        }48    }4950    return;51} 52int main(){53    freopen("input10.txt", "r", stdin);54    freopen("output10.txt", "w", stdout);55    scanf("%d%d",&n, &m);56    int x, y, z;57    for (int i = 1; i <= m; i ++){58        scanf("%d %d %d", &x, &y, &z);59        add(x, y, z);60    }61    sort(c + 1, c + 1 + m, comp);//快速排序 62    kruscal();63    printf("%d\n", ans);64    return 0;65}

原文地址:https://www.cnblogs.com/infroad/p/9245794.html

时间: 2024-10-10 14:54:59

【算法】关于图论中的最小生成树(Minimum Spanning Tree)详解的相关文章

Prim算法和Kruskal算法(图论中的最小生成树算法)

最小生成树在一个图中可以有多个,但是如果一个图中边的权值互不相同的话,那么最小生成树只可能存在一个,用反证法很容易就证明出来了. 当然最小生成树也是一个图中包含所有节点的权值和最低的子图. 在一个图中权值最小的那个边一定在最小生成树中,如果一个图包含环,环中权值最大的边一定不在最小生成树中,还有就是连接图的任意两个划分的边中权值最短的那一条一定在最小生成树中. 下面介绍两个算法. Prim算法 Prim算法就是以任意一个点为源点,将所有点分为两组,一组是已经在最小生成树上的点,另一组是还未在最小

最小生成树 (Minimum Spanning Tree,MST) ---Kruskal算法

引导问题: 假设要在N个城市之间建立通信联络网,则连通N个城市只需要N - 1条线路.这时,自然会考虑这样一个问题,如何在最省经费的前提下建立这个通信网. 基于问题所建立的定义: 可以用联通网来表示N个城市以及N个城市之间可能设置的连通线路,其中网的顶点表示城市,边表示两城市之间的线路,赋予边的权值表示相应的代价.对于N个顶点的连通网可以建立许多不同的生成树,每一棵生成树都可以是一个通信网.现在,要选择这样一颗生成树,也就是使总的耗费最少,这个问题就是构造连通网的的最小代价生成树的问题,即最小生

算法练习:最小生成树 (Minimum Spanning Tree)

(注:此贴是为了回答同事提出的一个问题而匆匆写就,算法代码只求得出答案为目的,效率方面还有很大的改进空间) 最小生成树是指对于给定的带权无向图,需要生成一个总权重最小的连通图.其问题描述及算法可以详见:https://en.wikipedia.org/wiki/Minimum_spanning_tree以下我选用其中一个简单的算法描述,编写 Python 代码尝试解决此问题. 下面是同事提出的问题的原图: 程序: 1 # coding: utf-8 2 3 from sets import Se

Geeks : Kruskal’s Minimum Spanning Tree Algorithm 最小生成树

寻找图中最小连通的路径,图如下: 算法步骤: 1. Sort all the edges in non-decreasing order of their weight. 2. Pick the smallest edge. Check if it forms a cycle with the spanning tree formed so far. If cycle is not formed, include this edge. Else, discard it. 3. Repeat st

11.5最小生成树(Minimum Spanning Trees)

11.5最小生成树(Minimum Spanning Trees) 对加权图求使得权值和最小的生成树,即为最小生成树,基于以点为基准和以边为基准,有两种求最小生成树的方法:Prim算法和Kruskal 最小生成树的具体算法实现 原文地址:https://www.cnblogs.com/SpicyArticle/p/12150738.html

【HDU 4408】Minimum Spanning Tree(最小生成树计数)

Problem Description XXX is very interested in algorithm. After learning the Prim algorithm and Kruskal algorithm of minimum spanning tree, XXX finds that there might be multiple solutions. Given an undirected weighted graph with n (1<=n<=100) vertex

HDU 4408 Minimum Spanning Tree 最小生成树计数

Minimum Spanning Tree Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) [Problem Description] XXX is very interested in algorithm. After learning the Prim algorithm and Kruskal algorithm of minimum spanning tree, XXX

[思维]Minimum Spanning Tree

题目描述 In the mathematical discipline of graph theory, the line graph of a simple undirected weighted graph G is another simple undirected weighted graph L(G) that represents the adjacency between every two edges in G. Precisely speaking, for an undire

CF609E. Minimum spanning tree for each edge

题解:随便构造一颗最小生成树 然后对于其他不在树上的边  考虑到 删除这条链上的最大值在把这条边加上去 能得到这条边所在的最小生成树 可以LCT维护 但是明显这个题是静态的树就没必要LCT 当然我觉得最优的是树剖以后ST nlogn的的复杂度 也可以树剖+线段树nlog^2的复杂度 #include <bits/stdc++.h> const int MAXN=2e5+10; #define ll long long using namespace std; ll read(){ ll x=0