并查集、Kruskal算法与Prim算法
2014-03-31 21:25
204 查看
并查集:
【用途】并查集是用来整理图的连通分量的,并查集过程完成后,可以确定两点是否连通,也可得知有多少个连通分量。推广到集合论里就是已知若干对元素,a在某集合里能推知b在集合里,用并查集确定各个元素归属于哪个集合,也可以顺便确定集合的数量。
【思想】并查集的思想其实很简单,每次合并都是尝试把两棵树合并。
如果树根相同说明其实这是一棵树,不操作;如果树根不同说明这确实是两棵树,把其中一棵的根挂在另一棵的根上视作合并。
【优化】
路径压缩:并查集有一些优化方法,普遍使用的是路径压缩,就是每次在查询根结点是谁时,把一路上遇到的结点都直接挂在根上,下次查就能更快找到根。
秩优化:就是在合并两棵树时把结点更少的树挂在结点多的树下。设a树有n个子结点,b树有m个,n<m。
a挂b上,n个点找根时路径长度增加了1;
b挂a上,m个点找根时路径长度增加了1.
这样秩优化就省下m-n个找根步骤。
(实际上,路径压缩已经是一个足够强的优化以至于可以将秩优化的效果忽略)
Kruskal:
【用途】用来求稀疏图的最小生成树,比起prim来,只用排一次序,效率也不错,那么会了kruskal何必还要学prim呢?prim适合在描述图用邻接矩阵而不逐边描述时。但是既然是用邻接矩阵描述图,那就是个稠密图,图比较大时,逐边抽出来再排序,相比起来prim就占便宜一些了。
【思想】把所有的边都排序,从小到大挨个往图里放,放了会成环就跳过这条边,假设有n个点,那么放过n-1条边最小生成树就建好了。
【耗时点】所有边按权排序。
Prim:
【用途】前面讲kruskal时说了,prim是用来求稠密图的最小生成树的。
【思想】建立一个点集,找任一点放入点集,遍历跟点集中点临接且不在点集中的点,找连边权值最小的那个点加入点集,然后再扩展。所有点都加入点集时最小生成树就完成了。
【耗时点】每一次扩展时的遍历。
下面是我按照算法思想写的朴素算法,O(N^3),适合用来理解,不适合实际使用:
【用途】并查集是用来整理图的连通分量的,并查集过程完成后,可以确定两点是否连通,也可得知有多少个连通分量。推广到集合论里就是已知若干对元素,a在某集合里能推知b在集合里,用并查集确定各个元素归属于哪个集合,也可以顺便确定集合的数量。
【思想】并查集的思想其实很简单,每次合并都是尝试把两棵树合并。
如果树根相同说明其实这是一棵树,不操作;如果树根不同说明这确实是两棵树,把其中一棵的根挂在另一棵的根上视作合并。
【优化】
路径压缩:并查集有一些优化方法,普遍使用的是路径压缩,就是每次在查询根结点是谁时,把一路上遇到的结点都直接挂在根上,下次查就能更快找到根。
秩优化:就是在合并两棵树时把结点更少的树挂在结点多的树下。设a树有n个子结点,b树有m个,n<m。
a挂b上,n个点找根时路径长度增加了1;
b挂a上,m个点找根时路径长度增加了1.
这样秩优化就省下m-n个找根步骤。
(实际上,路径压缩已经是一个足够强的优化以至于可以将秩优化的效果忽略)
int father ; //开始一系列并查集操作前把father数组初始化为全0 int get_father( int x ) { return (!father[x])?x:( father[x] = get_father(father[x]) ); } void Union( int x , int y ) //并查集处理有向图,Union两个点时注意让x为前驱点,无向图无所谓 { int fx = get_father(x); int fy = get_father(y); if( fx != fy ) father[fy] = fx; }既然说了秩优化我就忍不住手痒要写一下了:
int father , rank ; //开始一系列并查集操作前把father和rank数组初始化为全0 int get_father( int x ) { return (!father[x])?x:( father[x] = get_father(father[x]) ); } void Union( int x , int y ) //有向图就别用秩优化了 { int fx = get_father(x); int fy = get_father(y); if( fx != fy ) { if( rank[fy] < rank[fx] ) { father[fy] = fx; rank[fx] += rank[fy]; } else { father[fx] = fy; rank[fy] += rank[fx]; } } }
Kruskal:
【用途】用来求稀疏图的最小生成树,比起prim来,只用排一次序,效率也不错,那么会了kruskal何必还要学prim呢?prim适合在描述图用邻接矩阵而不逐边描述时。但是既然是用邻接矩阵描述图,那就是个稠密图,图比较大时,逐边抽出来再排序,相比起来prim就占便宜一些了。
【思想】把所有的边都排序,从小到大挨个往图里放,放了会成环就跳过这条边,假设有n个点,那么放过n-1条边最小生成树就建好了。
【耗时点】所有边按权排序。
#include<cstdio> #include<cstring> #include<vector> #include<algorithm> using namespace std; const int N = 100; struct edge { edge( int A , int B , int V ):a(A),b(B),v(V){} int a , b , v; bool operator < ( const edge &e )const { return v < e.v; } }; vector<edge> list; int father , ans , eN; int get_father( int x ) { return (!father[x]) ? x : ( father[x] = get_father( father[x] ) ); } void Union( int x , int y , int v ) { int fx = get_father(x); int fy = get_father(y); if( fx == fy ) return; father[fx] = fy; ans += v; eN++; } void kruskal( int n ) { eN = 0; memset( father , 0 , sizeof(father) ); for( vector<edge>::iterator it = list.begin() ; it != list.end() ; it++ ) { if( eN == n-1 ) return; Union( (*it).a , (*it).b , (*it).v ); } } int main() { int n , t , a , b , v; while( scanf("%d",&n) , n ) { t = n*(n-1)/2; ans = 0; while(t--) { scanf("%d%d%d",&a,&b,&v); list.push_back( edge( a , b , v ) ); } sort( list.begin() , list.end() ); if( n > 1 ) kruskal( n ); printf("%d\n",ans); list.clear(); } return 0; }
Prim:
【用途】前面讲kruskal时说了,prim是用来求稠密图的最小生成树的。
【思想】建立一个点集,找任一点放入点集,遍历跟点集中点临接且不在点集中的点,找连边权值最小的那个点加入点集,然后再扩展。所有点都加入点集时最小生成树就完成了。
【耗时点】每一次扩展时的遍历。
下面是我按照算法思想写的朴素算法,O(N^3),适合用来理解,不适合实际使用:
#include<cstdio> #include<vector> using namespace std; const int N = 100; int g , n; int prim() { bool ok = {false}; int sum = 0 , tmp , min_v; vector<int> s; s.push_back(0); ok[0] = true; while( s.size() < n ) { tmp = -1; for( int i = 0 ; i < s.size() ; i++ ) { for( int j = 0 ; j < n ; j++ ) { if( ok[j] || s[i] == j ) continue; if( tmp == -1 || tmp > 0 && g[s[i]][j] < tmp ) { tmp = g[s[i]][j]; min_v = j; } } } sum += tmp; s.push_back(min_v); ok[min_v] = true; } return sum; } int main() { while( ~scanf("%d",&n) ) { for( int i = 0 ; i < n ; i++ ) for( int j = 0 ; j < n ; j++ ) scanf("%d",&g[i][j]); printf("%d\n",prim()); } return 0; }
//这是我补充了邻接矩阵初始化的poj1789代码,O(N^2),可当模板 #include<cstdio> const int INF = 8; //边权值不超过7 const int N = 2000; //最多可达2000个点 char t [8]; int g , n; //建立邻接矩阵 int closestV ; //closestV表示点状态和距离最近的点 int lowCost ; //存储连边中的最小权值,跟closestV //一起描述每个点与邻接点的关系 //Prim算法 int Prim() { //初始化 int selectVNum = 1; int ans = 0; int newV = 0; //将点0作为新选中的点 for( int i = 0 ; i < n ; i++ ) closestV[i] = 0; //最近点都先指向起点等待更新 closestV[0] = -1; //closestV[i]=-1表示选中点i for( int i = 0 ; i < n ; i++ ) lowCost[i] = INF; //连边最小权值置为无限等待更新 //扩展选中点集 while(selectVNum < n) { int minCost = INF , VtoAdd; for( int i = 0 ; i < n ; i++ ) if(closestV[i] != -1)//不在选中点集中 { int cost = g[newV][i]; if(cost < lowCost[i]) { //更新邻接信息,O(N^3)到O(N^2)的关键 lowCost[i] = cost; closestV[i] = newV; } if(lowCost[i] < minCost) minCost = lowCost[VtoAdd=i]; } ans += minCost; closestV[newV=VtoAdd] = -1; selectVNum++; } return ans; } void initG() { for( int i = 0 ; i < n ; i++ ) for( int j = 0 ; j < n ; j++ ) g[i][j] = INF; } int main() { while( scanf("%d", &n) , n ) { for( int i = 0 ; i < n ; i++ ) scanf("%s", t[i]); //initG(); //如果邻接矩阵没被权值填满就需要初始化 for( int i = 0 ; i < n ; i++ ) for( int j = i+1 ; j < n ; j++ ) { int d = 0; //获得边权值 for( int k = 0 ; k < 7 ; k++ ) if( t[i][k] != t[j][k] ) d++; //向邻接矩阵赋边权值,看得出来是稠密图,相当稠密 g[i][j] = d; g[j][i] = d; } printf("The highest possible quality is 1/%d.\n", Prim()); } return 0; }
相关文章推荐
- 动易2006序列号破解算法公布
- C#数据结构与算法揭秘二
- 浅析STL中的常用算法
- JavaScript 组件之旅(二)编码实现和算法
- java数据结构和算法学习之汉诺塔示例
- python基础教程之python消息摘要算法使用示例
- php的hash算法介绍
- 将15位身份证补全为18位身份证的算法示例详解
- C++算法系列之日历生成的算法代码
- 利用线性表的顺序结构求集合的并、交、差、补(C语言实现)
- 1 2 3 4 5 6 7 8 9 = 110的java实现
- Sedgewick之巨著《算法》,与高德纳TAOCP一脉相承
- 【代码】Pythonの代码片段
- STL中算法
- 数据结构&算法学习
- 算法的时间复杂度
- 算法导论:选择排序的原理与实现
- PHP实现四种常用的排序算法
- 图解插入排序算法
- 一些常见算法的JavaScript实现