详解最小生成树代码C++


首先介绍什么是生成树。

生成树是相对于图的,假设图 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=VGETEG
注意生成树的节点与图的节点相同,不是子集关系。

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

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

下图就是连通图的一个最小生成树,注意最小生成树并不唯一
在这里插入图片描述

如何构造最小生成树

利用贪心的策略,每一个时刻都生长出最小生成树的一条边,并在整个循环过程中,边的集合 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 GENERICMST(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,VS) 是集合 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 VS 中,则称该条边横跨切割 ( S , V − S ) (S,V-S) (S,VS)

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

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

例如,在下面图中,存在横跨切割 ( S , V − S ) (S,V-S) (S,VS) 的边有
( 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,VS) 尊重集合 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,VS) 是图 G G G 中尊重集合 A A A 的任意一个切割,又设 ( u , v ) (u,v) (u,v) 是横跨切割 ( S , V − S ) (S,V-S) (S,VS) 的一条轻量级边,则边 ( u , v ) (u,v) (u,v) 对于集合 A A A 是安全的。

我们可以这样理解 G E N E R I C − M S T GENERIC-MST GENERICMST 算法,在算法推进过程中,集合 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 1vexNum,边的下标用 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<V2,则有 l o g ∣ E ∣ = O ( l o g ∣ V ∣ ) log|E|=O(log|V|) logE=O(logV),所以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 1000

struct 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();
}
  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

rebibabo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值