个人总结---连通图的最小生成树算法

 

     最近在复习数据结构和算法的的内容,栈和队列的思想是比较深刻,借于许多高级语言都有相应的框架实现了栈和队列链表等,所以对于这一类,我们只需要了解其思想,在真正操作时,也会显得比较简单。但是还有一类数据结构是稍显复杂的,在高级语言的程序里面并没有相应的框架,比如树和图。树一般可用节点结构体来封装一个节点,但是图,图的话就不容易表示了,因为图是无序的,每个节点与其他节点都有任意的连通性。但是基于使用图的操作目的而言,一般有:搜索(遍历)、最小生成树、寻找节点之间的最小路径等。其目的都是为了存储点对之间的连通性,以及通路的代价,为此,我们可以根据我们的使用目的对其进行抽象为:邻接表、邻接矩阵、十字链表。

连通图的最小生成树

  最小生成树其实在计算机网络里面也有应用:在有线Lan中,为避免交换机之间的连线形成环路,而最终会导致“兜圈子”,从而引起“广播风暴”的现象,Lan中交换机的配置就采用了最小生成树的算法,来避免形成环路。下面介绍两种连通图的最小生成树算法,普里姆算法(Prim)和克鲁斯卡算法(Kruskal),他们在时空消耗上面,各有优劣。但是这里也顺便说,Prim和Kruskal算法都是具是贪心算法的类比,都是从局部最优最后到全局最优的。

(Prim)普里姆算法

  其思想是:

1.有两个集合V,S  .  S代表已经被识别的最小生成树路径上的节点集合,V代表所有节点的集合,V-S 就是剩余未被识别的节点的集合。

2.程序开始时,指定v0 加入S中,使得{v0} = S .

3.在V-S 集合中寻找到下一个节点vi,使得vi 到 S的距离最短。(vi到S的距离是指,vi到S集合中任意一点的距离;当两点直接相连时为连通,否则距离为无穷)。将vi 加入到集合S中。

4.不断运行步骤三,直到S集合包含了所有节点。

  由上就是普里姆算法,其思想非常简单,每次都是去取寻找离已识别集合最短的路径,这样局部最优导致全局最优。该算法的时间复杂度为O(n2).

  下面给出完整的C++代码实现:

  

#include <iostream>
#include<vector>
#include<algorithm>
#include<set>
#include<string.h>
#define N 6
#define MAX_INT 999999
using namespace std;

// 边的结构体 
typedef struct{
	int x;
	int y;
	int cost;
} Tpath;

//连通图的邻接矩阵
int g [N][N] = {{-1,6,1,5,-1,-1},{6,-1,5,-1,3,-1},{1,5,-1,5,6,4},{5,-1,5,-1,-1,2},{-1,3,6,-1,-1,6},{-1,-1,4,2,6,-1}};

void gprim(); 

//main
int main(int argc, char** argv) {
	gprim();

	return 0;
}
//运行prim算法
void gprim(){
	vector<Tpath> p;   //记录边
	vector<int>u;      //集合S
	u.push_back(0);    //将V0加入到S中
	int node1= 0,node2 = 0,cost = 0;
	int i = 0;
	vector<int>::iterator  it;
	for(u.size(); u.size() < N ;){
		// get the lowcost path
		node1 = -1;
		node2 = -1;
		cost = MAX_INT ;
		for(it = u.begin() ;it != u.end() ; it++){      // 从V-S集合里面寻找到离S集合最lowcost的节点和对应的边。将其记录下来为 为cost,node1,node2 
			int k = (*it);
			for(i = 0; i < N ;i++){
				if(i == (*it))continue;
				if(g[k][i] >= 0 && (find(u.begin(),u.end(),i) == u.end()) && g[k][i] < cost){
					node1 = k;
					node2 = i;
					cost = g[k][i];
				}
			}
		}
		
		// 将该节点加入到S中 并记录下路径path
		Tpath path;
		path.cost = cost;
		path.x = node1;
		path.y = node2;
		p.push_back(path);
		u.push_back(node2);		
	}
	//输出
	vector<Tpath>::iterator itO;
	for(itO = p.begin() ; itO != p.end() ;itO++){
		printf("(%d ,%d) cost: %d\n",itO->x+1,itO->y+1,itO->cost);
	}
}

  

(Kruskal)克鲁斯卡算法

  其思想是:

1.引入节点的连通分量的概念:即一个节点与其他哪些节点相连通。

2.程序开始时,每个节点的连通分量就是自己。有集合E,SE,S。E为图中边的集合,SE为图中已经被识别的边的集合。SE开始为{},S为已识别点的集合。

3.从E-SE中选择一条边(vi,vj),其边的两个顶点时是vi,vj:该边的距离是所有E-S中距离最短的。同时,vi的连通分量中不包含vj,vj的连通分量中不包含vi。将(vi,vj)加入到SE中,将vi,vj

加入到S中,同时将vi的连通分量加入vj中,将vj的连通分量加入到vi中。

4.持续运行步骤3,直到S集合包含了所有节点。

  由上就是克鲁斯卡算法。分析其算法可知,其时间复杂度度为n(logn) , n 为连通图中边的个数。为什么是O(n(logn))呢?其实很简单,克鲁斯卡的算法中每次都是找的E-SE中最短的边,这里可以使用排序算法对所有的边进行排序(O(nlogn)),然后再执行算法步骤2-4时,就可以依次取出来(O(n))。而这里最大的时间消耗是排序,所以是O(nlogn)。

  Kruskal的个人实现:

#include <iostream>
#include<vector>
#include<algorithm>
#include<set>
#include<string.h>
#define N 6
#define MAX_INT 999999
using namespace std;
 
// 边的结构体
typedef struct{
    int x;
    int y;
    int cost;
} Tpath;
 
//连通图的邻接矩阵
int g [N][N] = {{-1,6,1,5,-1,-1},{6,-1,5,-1,3,-1},{1,5,-1,5,6,4},{5,-1,5,-1,-1,2},{-1,3,6,-1,-1,6},{-1,-1,4,2,6,-1}};
//increase sort 
bool cmp(const Tpath &p1, const Tpath &p2){
	return p1.cost < p2.cost;
}

void gkruskal();

//main
int main(int argc, char** argv) {
    gkruskal();
 
    return 0;
}

void gkruskal(){
	vector<Tpath> t;
	int i = 0 ,j = 0;
//将所有的边生成一个一个的结构体节点
	for(i ; i < N ; i++){
		for(j = i+1;j <N ;j++){
			if(g[i][j] < 0) continue;
			Tpath p ;
			p.x = i;
			p.y = j;
			p.cost = g[i][j];
			t.push_back(p);
		}
	}
//按边的距离升序排序
	sort(t.begin(),t.end() ,cmp);
	vector<Tpath>::iterator it;

	
//为每个节点Vi设置连通分量
	vector< set<int> > sets;
	for(i = 0; i < N ;i++){
		set<int> v;
		v.insert(i);
		sets.push_back(v);
	}
	vector<Tpath> p;
	 i = 0;
//执行算法,扫描升序边集合
	for(;p.size() < N -1 ; ){
		set<int> x  = sets[t[i].x];
		set<int> y =  sets[t[i].y];
		set<int>::iterator it;

     //如果该边的两个顶点Vi ,Vj 各自的连通分量不包含对方,就将改变加入到路径集合SE中
		if(x.find(t[i].y) == x.end()){
			p.push_back(t[i]);
			
			set<int>::iterator xi ;
                   //同时将Vj的连通分量加入到Vi的连通分量重
			for(xi = sets[t[i].x].begin() ; xi != sets[t[i].x].end(); xi++){
				if((*xi) == t[i].x)continue;
				sets[(*xi)].insert(y.begin(),y.end());
			}
                      //同时将Vi的连通分量加入到Vj的连通分量重
			for(xi = sets[t[i].y].begin() ; xi != sets[t[i].y].end(); xi++){
				if((*xi) == t[i].y)continue;
				sets[(*xi)].insert(x.begin(),x.end());
			}
			sets[t[i].x].insert(y.begin(),y.end());
			sets[t[i].y].insert(x.begin(),x.end());
		}
	
			++i;   //扫描下一条边
	}
	//输出最小生成树的 边对
	for(it = p.begin() ; it != p.end();it++){
		cout<<"("<< it->x +1 <<","<<it->y + 1<<")"<<"cost :"<<it->cost<<endl;
	}
	
}        

  

  

  上面就是两个比较简单,但是比较经典的连通图最小生成树的算法。Prim算法时间复杂度略高,但是空间消耗较少;而Kruskal的算法呢,时间复杂度低,但需要为每个节点设置连通分量的存储空间,因此空间复杂度略高。总之看了这些算法之后,总是对计算机的算法设计有股莫名的倾佩和向往啊!。。。

参考书本:

      数据结构(c语言版) 清华大学出版社

      计算机算法设计与分析

转载于:https://www.cnblogs.com/compilers/p/5450087.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值