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


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

    连通图的最小生成树

      最小生成树其实在计算机网络里面也有应用:在有线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
    ",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语言版) 清华大学出版社

          计算机算法设计与分析

  • 相关阅读:
    计算日期之差
    大数相加
    NY-字符串替换
    HDU1004之总是wa的细节问题
    指针在字符串简单应用
    mybatis~SQL映射
    java实现递归(1)
    apk、图片下载工具(1)
    签到规则工具(1)
    短信发送工具(2)
  • 原文地址:https://www.cnblogs.com/compilers/p/5450087.html
Copyright © 2020-2023  润新知