文章目录

  • 如何构造最小生成树
  • 一些定义
  • 定理及其推论
  • 图的代码部分
  • Kruskal算法
    • 并查集的代码部分
    • Kruscal代码
  • Prims算法
  • 完整代码

首先介绍什么是生成树。

生成树是相对于图的,假设图 G = ( V G , E G ) G=(V_G,E_G) G=(VG​,EG​),图 G G G 的生成树 T = ( V T , E T ) T=(V_T,E_T) T=(VT​,ET​) 是图 G G G 的子图,且满足
V T = V G E T ⊆ E G V_T=V_G\\ E_T\subseteq E_G VT​=VG​ET​⊆EG​
注意生成树的节点与图的节点相同,不是子集关系。

对于带权图来说,生成树的成本等于树所有边的权重之和,由此可以推出最小生成树的定义:

最小生成树:具有最小权重的生成树

下图就是连通图的一个最小生成树,注意最小生成树并不唯一

如何构造最小生成树

利用贪心的策略,每一个时刻都生长出最小生成树的一条边,并在整个循环过程中,边的集合 A A A 都要满足循环不变式

在每遍循环前, A A A 是某棵最小生成树的一个子集。

处理策略:每一步,我们选择一条边 ( u , v ) (u,v) (u,v) 加入集合 A A A,使得 A A A 不违反循环不变式,则 A ∪ { ( u , v ) } A\ \cup\{(u,v)\} A ∪{(u,v)} 还是某棵最小生成树的子集,我们把这样的边叫做安全边

我们可以得出最小生成树的基本生成算法:
G E N E R I C − M S T ( G , ω ) w h i l e ( A 还 不 是 一 颗 最 小 生 成 树 ) 找 到 一 条 安 全 边 ( u , v ) A = A ∪ { ( u , v ) } r e t u r n A GENERIC-MST(G,\omega)\\\ \ \ while(A还不是一颗最小生成树)\\ \quad 找到一条安全边(u,v)\\ \quad A=A\ \cup\{(u,v)\}\\ return\ A\qquad\qquad\qquad\qquad GENERIC−MST(G,ω)   while(A还不是一颗最小生成树)找到一条安全边(u,v)A=A ∪{(u,v)}return A
那么如何判断一条边到底是不是最小生成树的一条边,即安全边呢?

一些定义

首先引入一些定义,最后我们可以得出寻找安全边的定理。

切割:无向图 G = ( V , E ) G=(V,E) G=(V,E) 的一个切割 ( S , V − S ) (S,V-S) (S,V−S) 是集合 V V V 的一个划分。

说白了就是将原来图的节点分成两半,下图的切割将图的节点划分成了 { { a , b , d , e } , { c , f , g , h , i } } \{\{a,b,d,e\},\{c,f,g,h,i\}\} {{a,b,d,e},{c,f,g,h,i}}。

横跨切割:如果一条边 ( u , v ) ∈ E (u,v)\in E (u,v)∈E 的一个端点在集合 S S S 中,另一个端点在集合 V − S V-S V−S 中,则称该条边横跨切割 ( S , V − S ) (S,V-S) (S,V−S)。

尊重:如果边集 A A A 中不存在横跨某切割的边,则称该切割尊重集合 A A A。

轻量级边:在横跨一个切割的所有边中,权重最小的边称为轻量级边。

例如,在下面图中,存在横跨切割 ( S , V − S ) (S,V-S) (S,V−S) 的边有
( b , c ) , ( c , d ) , ( b , h ) , ( d , f ) , ( a , h ) , ( e , f ) (b,c),(c,d),(b,h),(d,f),(a,h),(e,f) (b,c),(c,d),(b,h),(d,f),(a,h),(e,f)
而边为 ( c , d ) (c,d) (c,d) 是唯一一条轻量级边,因为其在所有横跨切割的边中的权重最小。

途中阴影边构成集合 A A A,其中不存在横跨该切割的边,所有切割 ( S , V − S ) (S,V-S) (S,V−S) 尊重集合 A A A。

定理及其推论

定理:设 G = ( V , E ) G=(V,E) G=(V,E) 是一条有权无向图,设集合 A A A 为 E E E 的子集,且 A A A 包含于图 G G G 的某棵最小生成树中,设 ( S , V − S ) (S,V-S) (S,V−S) 是图 G G G 中尊重集合 A A A 的任意一个切割,又设 ( u , v ) (u,v) (u,v) 是横跨切割 ( S , V − S ) (S,V-S) (S,V−S) 的一条轻量级边,则边 ( u , v ) (u,v) (u,v) 对于集合 A A A 是安全的。

我们可以这样理解 G E N E R I C − M S T GENERIC-MST GENERIC−MST 算法,在算法推进过程中,集合 A A A 始终保持无环状态,且算法执行的任意时刻,图 G A G_A GA​ 是一个森林,其中的每一个连通分量都是一棵树。

而对于安全边 ( u , v ) (u,v) (u,v),由于 A ∪ { ( u , v ) } A\ \cup\{(u,v)\} A ∪{(u,v)} 必须无环,所以 ( u , v ) (u,v) (u,v) 必须连接的是森林 G A G_A GA​ 中的两个联通分量。

推论:设 G = ( V , E ) G=(V,E) G=(V,E) 是一个有权无向连通图,设集合 A A A 是 E E E 的一个子集,且 A A A 包含在图 G G G 的某棵最小生成树中。设 C = ( V C , E c ) C=(V_C,E_c) C=(VC​,Ec​) 为森林 G A = ( V , A ) G_A=(V,A) GA​=(V,A) 中的一个连通分量,边 ( u , v ) ∈ E , ( u , v ) ∉ A (u,v)\in E,(u,v)\notin A (u,v)∈E,(u,v)∈/​A,是 C C C 连接其他连通分量中权重最小的边,则边 ( u , v ) (u,v) (u,v) 对于集合 A A A 是安全的。

按照寻找安全边方法的不同,可分为Kruskal算法和Prim算法。

图的代码部分

我们用邻接表来存放边,其中节点下标为 1 − v e x N u m 1-vexNum 1−vexNum,边的下标用 c n t cnt cnt 来编号,每加入一条边, c n t cnt cnt 就加一,存放边的数组是 e d g e edge edge。

e d g e edge edge 存放了所有边,每一条边是结构体 n o d e node node, n e x nex next 表示该边在邻接表的下一条边的索引,相当于指针形式的 ∗ n e x t *next ∗next, t o to to 表示这条边指向的另一个节点标号, v v v 是边的权重。

h e a d [ i ] head[i] head[i] 存放的是节点 i i i 的第一条边在 e d g e edge edge 中的下标, v i s i t e d [ i ] visited[i] visited[i] 表示节点 i i i 是否遍历过, v e x N u m vexNum vexNum 为节点的个数。

class Graph{int cnt,head[MAX],visited[MAX],vexNum;       //用来给边命名,head[x]为节点x指向的第一条边 ,MAX为最大节点/边个数 struct node{int next,to,v;      //下一条边,该边指向的节点,边的长度 bool operator<(const node& n) const{return n.v<v;};}edge[MAX],edge_copy[MAX];

图新加入一条有向边是用的 a d d add add 函数,加入无向边用 a d d 2 add2 add2。

void add(int x,int y,int v){     //添加有向边 cnt++;                //边的命名cnt加一 edge[cnt].to=y;        //该边指向节点为y edge[cnt].v=v;edge[cnt].next=head[x];  //在链表头插入新边 head[x]=cnt;            //新边插入x的第一条边
}void add2(int x,int y,int v){      //添加无向边 add(x,y,v);add(y,x,v);
}

Kruskal算法

寻找安全边的方法:在所有连接森林中两棵不同树的边中,找权重最小的边 ( u , v ) (u,v) (u,v),依照的是上述的推论。

具体算法如下:

初始化森林,森林当中的每一颗树都是一个节点
将所有边按照权重按照从小到大的顺序排序
for 所有的边,依次取出权值最小的边(u,v)if u和v不在同一个连通分量then 将边(u,v)加入边集合A当中,并且连接u和v
return A

初始情况 A = ∅ A=\varnothing A=∅, G A G_A GA​是一个森林,该森林的每一棵树都是图中的一个节点。

我们拿下面的图来举例,权重最小的边是 ( h , g ) (h,g) (h,g),我们把它加入到 A A A 中,图中用粗灰线表示。

我们再选取最小权重的边 ( i , c ) (i,c) (i,c),把它加入到 A A A 中。

下面依次展示了该算法的全过程


因为添加完 ( b , c ) (b,c) (b,c) 以后会成环 a , b , c , g , h a,b,c,g,h a,b,c,g,h,所以不将边 ( b , c ) (b,c) (b,c) 加入 A A A 中。




遍历完所有边以后,我们得到了最小生成树,权重之和为37。

我们首先将所有边按照权重排序,时间复杂度为 O ( E l o g E ) O(ElogE) O(ElogE),我们使用并查集的方式来表示森林,如果不知道并查集的小伙伴可以看我的另一篇博客,(199条消息) 并查集C++实现——算法设计与分析,含代码解释_rebibabo的博客-CSDN博客,我们依次取出排好序的边,如果边连接两个连通分量,则我们将这两个联通分量相连,否则我们跳过,不然就会形成环了,我们一共要遍历 ∣ E ∣ |E| ∣E∣ 次,每一次我们都合并了两个森林,时间复杂度参考并查集的union操作,是 O ( l o g E ) O(logE) O(logE) 的,所以循环的时间复杂度也是 O ( E l o g E ) O(ElogE) O(ElogE)。

考虑到 ∣ E ∣ < ∣ V ∣ 2 |E|<|V|^2 ∣E∣<∣V∣2,则有 l o g ∣ E ∣ = O ( l o g ∣ V ∣ ) log|E|=O(log|V|) log∣E∣=O(log∣V∣),所以Kruskal算法的时间复杂度可以表示为 O ( E l o g V ) O(ElogV) O(ElogV)。

并查集的代码部分

合并两个联通图的操作是connect,判断是否属于同一个连通图使用isConnected。

class UnionFind {int id[MAX]; // int contain[MAX]; // 包含多少节点 int minIndex; // 范围int maxIndex; // 范围int cnt; //连通分量的个数public :UnionFind() {}UnionFind(int minIndex, int maxIndex) {this->minIndex = minIndex;this->maxIndex = maxIndex;this->cnt = maxIndex - minIndex + 1; // 连通分量的个数for (int i = minIndex; i <= maxIndex ; i++) {id[i] = i;contain[i] = 1;}}int getRoot(int p) {//采用递归的方式 if (id[p] == p) { //自己就是根节点 return p;  } else {int d = id[p];int root = getRoot(d);if (d != root) {id[p] = root;      //将当前节点的id设置成根节点 contain[d] -= contain[p];     //因为d的子树移到了根节点,所以要将d的contain减去p的contain }return root;}}bool isConnected(int p, int q) {return getRoot(q) == getRoot(p);}bool connect(int p, int q) {int pRoot = getRoot(p);int qRoot = getRoot(q);if (qRoot == pRoot) {return false; // 已经在同一个set里面了,已经在同一个连通分量里面了} else {if(contain[p] >= contain[q]) {id[qRoot] = pRoot;contain[pRoot] += contain[qRoot];} else {id[pRoot] = qRoot;contain[qRoot] += contain[pRoot];}}cnt --; //连通分量少1}
};

Kruscal代码

首先将所有边按照权重排序,排序会打乱边的顺序,所以拷贝一份放在 e d g e _ c o p y edge\_copy edge_copy 里再将排好序的边表示为 ( l , r , v ) (l,r,v) (l,r,v),放在 v e c t o r e vector\ e vector e 中, 然后建立森林 u f uf uf,每一个连通分量为一个节点,然后依次遍历排好序的边集合 e e e,每次从中选取权值最小的边,如果这条边的两端 l , r l,r l,r 横跨两个连通分量,即 u f . i s C o n n e c t ( ) uf.isConnect() uf.isConnect() 为 f a l s e false false ,则将这两节点所在的联通分量连起来,执行 u f . c o n n e c t uf.connect uf.connect 操作,如果这条边不横跨两个连通分量,则跳过,依次循环 ∣ E ∣ |E| ∣E∣ 遍,具体代码和注释如下:

int Kruskal(){cout<<"最小生成树:";int sum=0;memcpy(edge_copy,edge,sizeof(edge)); //拷贝边edge一份,排序会打乱原来边的顺序sort(edge_copy,edge_copy+MAX);           //将边按照权重排序vector<vector<int> >edges;                //将edge改成所有边的集合(l,r,v),表示左节点,右节点和权重for(int i=0;i<MAX&&edge_copy[i].v;i+=2){     //按照边的长度从大到小排序,遇到0则结束 vector<int> e;//sort函数不会改变原来的顺序,所以连续两个相同大小的边的两个to节点组成一条的两端节点 e.push_back(edge_copy[i].to), e.push_back(edge_copy[i+1].to), e.push_back(edge_copy[i].v); edges.insert(edges.begin(),e);//逆序插入 }UnionFind uf(1,MAX);           //构建所有节点为单独一棵树的森林 for(int i=0;i<edges.size();i++){    //遍历所有边 if(!uf.isConnected(edges[i][0],edges[i][1])){   //如果这两棵树不相通 uf.connect(edges[i][0],edges[i][1]);//则连接这两个树,且连接两棵树的该边是一条安全边,即权重最小且不会形成环 sum+=edges[i][2];     //计算权重和printf("(%d,%d,%d)",edges[i][0],edges[i][1],edges[i][2]);}} cout<<endl<<"最小生成树路径之和为:"<<sum<<endl;memset(visited,0,sizeof(visited));return sum;
}

Prims算法

简单来说,Prim算法每一步都在连接集合 A A A 和 A A A 之外所有节点的边当中,权重最小的边,参考了上面的定理,具体算法如下

任意选择一个节点a开始,将a的所有邻边放在待选边集合E当中
while E不为空u是E中权值最小的边对应的另一个节点for u的所有邻边(u,v)if v没有访问过then 将(u,v)加入E中

就拿下图打比方,假设一开始的节点为 a a a,我们选择与 a a a 相邻的所有边当中权值最小的边 ( a , b ) (a,b) (a,b),将 b b b 标记为访问过。

接着,我们从和 a , b a,b a,b 相邻的所有边当中选取一个最短的边, ( b , c ) (b,c) (b,c) 或者 ( a , h ) (a,h) (a,h) 都可以,我们暂且选取 ( b , c ) (b,c) (b,c)。

接着我们选择和 a , b , c a,b,c a,b,c 相邻的所有边当中权重最小的,选择 ( c , i ) (c,i) (c,i),依次类推,如果这条边的另一端的节点访问过,则选择次大的边。

如果我们考虑使用优先队列的话,能够使得每一次查找相邻权值最短边的时间缩短为 O ( l o g V ) O(logV) O(logV),而一共要遍历节点 ∣ V ∣ |V| ∣V∣ 次,所以Prim的时间复杂度为 O ( V l o g V ) O(VlogV) O(VlogV)

下面是Prim算法的代码

int Prim(int v0) {int sum=0, cur_node=v0;priority_queue<node> q;visited[v0]=1;      //设置visited for(int i=head[v0];i;i=edge[i].next){ //先将v0的所有邻边入队 q.push(edge[i]);}while(!q.empty()){node n=q.top();q.pop();if(visited[n.to]==0){        //该边是cur_node相邻边中权重最小的边,且还没有遍历过下一节点 cur_node=n.to;      //设置当前遍历到的节点,方便打印最小生成树 visited[n.to]=1; //设置visited sum+=n.v;for(int i=head[n.to];i;i=edge[i].next){q.push(edge[i]);    //把当前节点的所有邻边入队 }}}cout<<sum<<endl;memset(visited,0,sizeof(visited));return sum;
}

完整代码

#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
using namespace std;
#define MAX 1000struct vertex{int node;int dis;bool operator<(const vertex &n) const {return n.node<node;};
}; class UnionFind {int id[MAX]; // int contain[MAX]; // 包含多少节点 int minIndex; // 范围int maxIndex; // 范围int cnt; //连通分量的个数public :UnionFind() {}UnionFind(int minIndex, int maxIndex) {this->minIndex = minIndex;this->maxIndex = maxIndex;this->cnt = maxIndex - minIndex + 1; // 连通分量的个数for (int i = minIndex; i <= maxIndex ; i++) {id[i] = i;contain[i] = 1;}}int getRoot(int p) {//采用递归的方式 if (id[p] == p) { //自己就是根节点 return p;  } else {int d = id[p];int root = getRoot(d);if (d != root) {id[p] = root;      //将当前节点的id设置成根节点 contain[d] -= contain[p];     //因为d的子树移到了根节点,所以要将d的contain减去p的contain }return root;}}bool isConnected(int p, int q) {return getRoot(q) == getRoot(p);}bool connect(int p, int q) {int pRoot = getRoot(p);int qRoot = getRoot(q);if (qRoot == pRoot) {return false; // 已经在同一个set里面了,已经在同一个连通分量里面了} else {if(contain[p] >= contain[q]) {id[qRoot] = pRoot;contain[pRoot] += contain[qRoot];} else {id[pRoot] = qRoot;contain[qRoot] += contain[pRoot];}}cnt --; //连通分量少1}
}; class Graph{int cnt,head[MAX],visited[MAX],vexNum;       //用来给边命名,head[x]为节点x指向的第一条边 ,MAX为最大节点/边个数 struct node{int next,to,v;      //下一条边,该边指向的节点,边的长度 bool operator<(const node& n) const{return n.v<v;};}edge[MAX],edge_copy[MAX];public:Graph(int num){cnt=0;vexNum=num;memset(head,0,sizeof(head));memset(visited,0,sizeof(visited));memset(edge,0,sizeof(edge));}void add(int x,int y,int v){       //添加有向边 cnt++;                //边的命名cnt加一 edge[cnt].to=y;        //该边指向节点为y edge[cnt].v=v;edge[cnt].next=head[x];  //在链表头插入新边 head[x]=cnt;            //新边插入x的第一条边 }void add2(int x,int y,int v){     //添加无向边 add(x,y,v);add(y,x,v);}void show(){for(int i=0;i<MAX;i++){    //遍历各节点 for(int j=head[i];j;j=edge[j].next){  //如果j等于0,说明没有边了 printf("(%d,%d,%d)",i,edge[j].to,edge[j].v);}if(head[i])cout<<endl;}} int Prim(int v0) {int sum=0, cur_node=v0;priority_queue<node> q;visited[v0]=1;        //设置visited for(int i=head[v0];i;i=edge[i].next){ //先将v0的所有邻边入队 q.push(edge[i]);}while(!q.empty()){node n=q.top();q.pop();if(visited[n.to]==0){        //该边是cur_node相邻边中权重最小的边,且还没有遍历过下一节点 cur_node=n.to;      //设置当前遍历到的节点,方便打印最小生成树 visited[n.to]=1; //设置visited sum+=n.v;for(int i=head[n.to];i;i=edge[i].next){q.push(edge[i]);    //把当前节点的所有邻边入队 }}}cout<<sum<<endl;memset(visited,0,sizeof(visited));return sum;}int Kruskal(){cout<<"最小生成树:";int sum=0;memcpy(edge_copy,edge,sizeof(edge));sort(edge_copy,edge_copy+MAX);vector<vector<int> >edges;   //将edge改成所有边的集合(l,r,v) for(int i=0;i<MAX&&edge_copy[i].v;i+=2){       //按照边的长度从大到小排序,遇到0则结束 vector<int> e;//sort函数不会改变原来的顺序,所以连续两个相同大小的边的两个to节点组成一条的两端节点 e.push_back(edge_copy[i].to), e.push_back(edge_copy[i+1].to), e.push_back(edge_copy[i].v); edges.insert(edges.begin(),e);//逆序插入 }UnionFind uf(1,MAX);           //构建所有节点为单独一棵树的森林 for(int i=0;i<edges.size();i++){    //遍历所有边 if(!uf.isConnected(edges[i][0],edges[i][1])){   //如果这两棵树不相通 //则连接这两个树,且连接两棵树的该边是一条安全边,即权重最小且不会形成环 uf.connect(edges[i][0],edges[i][1]);sum+=edges[i][2];printf("(%d,%d,%d)",edges[i][0],edges[i][1],edges[i][2]);}} cout<<endl<<"最小生成树路径之和为:"<<sum<<endl;memset(visited,0,sizeof(visited));return sum;}
};int main(void){Graph g(6);int temp[9][3]={{1,6,14},{1,2,7},{1,3,9},{3,6,2},{2,3,10},{2,4,15},{3,4,11},{5,6,9},{4,5,6}};for(int i=0;i<9;i++)g.add2(temp[i][0],temp[i][1],temp[i][2]);g.show();g.Prim(1);g.Kruskal();
}

详解最小生成树代码C++相关推荐

  1. 调包侠福音!机器学习经典算法开源教程(附参数详解及代码实现)

    Datawhale 作者:赵楠.杨开漠.谢文昕.张雨 寄语:本文针对5大机器学习经典算法,梳理了其模型.策略和求解等方面的内容,同时给出了其对应sklearn的参数详解和代码实现,帮助学习者入门和巩固 ...

  2. 粒子群(pso)算法详解matlab代码,粒子群(pso)算法详解matlab代码

    粒子群(pso)算法详解matlab代码 (1)---- 一.粒子群算法的历史 粒子群算法源于复杂适应系统(Complex Adaptive System,CAS).CAS理论于1994年正式提出,C ...

  3. 图像质量损失函数SSIM Loss的原理详解和代码具体实现

    本文转自微信公众号SIGAI 文章PDF见: http://www.tensorinfinity.com/paper_164.html http://www.360doc.com/content/19 ...

  4. python 自动化-Python API 自动化实战详解(纯代码)

    主要讲如何在公司利用Python 搞API自动化. 1.分层设计思路 dataPool :数据池层,里面有我们需要的各种数据,包括一些公共数据等 config :基础配置 tools : 工具层 co ...

  5. 数学建模——智能优化之遗传算法详解Python代码

    数学建模--智能优化之遗传算法详解Python代码 import numpy as np import matplotlib.pyplot as plt from matplotlib import ...

  6. 数学建模——主成分分析算法详解Python代码

    数学建模--主成分分析算法详解Python代码 import matplotlib.pyplot as plt #加载matplotlib用于数据的可视化 from sklearn.decomposi ...

  7. 数学建模——智能优化之模拟退火模型详解Python代码

    数学建模--智能优化之模拟退火模型详解Python代码 #本功能实现最小值的求解#from matplotlib import pyplot as plt import numpy as np imp ...

  8. 数学建模——智能优化之粒子群模型详解Python代码

    数学建模--智能优化之粒子群模型详解Python代码 import numpy as np import matplotlib.pyplot as plt from mpl_toolkits.mplo ...

  9. 数学建模——支持向量机模型详解Python代码

    数学建模--支持向量机模型详解Python代码 from numpy import * import random import matplotlib.pyplot as plt import num ...

最新文章

  1. python基础代码-Python基础(代码)
  2. 【线性表4】线性表的链式实现:静态表
  3. 如何使用Python脚本
  4. Delphi面向对象学习随笔一:类与对象的关系
  5. “火星人”马斯克推论:世界或是被编码而成,上帝可能是个程序员!
  6. pytorch 可复现性
  7. Android Studio实现QQ的注册、登录和好友列表页面的跳转
  8. jupyterlab中使用conda虚拟环境
  9. Jfinal weixin源码分析---碎碎念(看最后,有福利)
  10. Pycharm安装Markdown插件
  11. AJAX,Axio异步框架(对原生AJAX封装)。web分区
  12. 代写品牌故事-品牌故事的结构
  13. Python 爬虫--下载音乐
  14. 能上QQ不能上浏览器处理方法(win11版)
  15. minigui 的中文字体部署及支持窗口模态、非模态
  16. 杭州师范大学c语言程序设计机试,杭州师范大学C语言试题第3套.pdf
  17. GFlags调试堆中野指针
  18. 10 个开源免费的电子商务平台
  19. mysql中的left join用法 (及多条件查询
  20. 电子书翻页效果(转)

热门文章

  1. arduino中 #define、const和int 的差别
  2. 文件夹无法访问如何解决?
  3. 实现自己的Tomcat、Servlet、多线程(线程池)处理请求
  4. Amazon Prime Video为《周四橄榄球之夜》推出新功能,让全球NFL球迷能够自定义流媒体播放体验
  5. createImage和getImage区别
  6. 第二十九节 C++ 继承之向基类传递参数
  7. 哈佛管理论丛-谁背上了猴子(转)
  8. Kotlin第一课Hello World —— Package、main、fun、import、变量、注释
  9. MIPS汇编二进制转10进制
  10. 四川大地震将是中国社会的转折点