以前学图论的时候感觉prim和kruskal理论都理解了,prim不就是基于点,kruskal不就是基于边嘛,自以为掌握了,但某一天发现需要自己写的时候却无从下手,所以还是决定把这两个算法写一遍。
最小生成树,就是以最小的代价将所有的点连通起来,打个比方就是将各个独立的村落修道路将所有村落连接起来,待会看图就可理解了。
关于这两算法的详细介绍可参考图的基本算法解析(最小生成树) - 综合编程类其他综合 - 红黑联盟,本文图是他的,代码也是参考他的依照自己的理解进行了一些小优化,下文会标出。
1、PrimPrim我个人的理解就是集合开始往外扩张。也就是部落从一个小村庄开始,他们想最有效的征服周围的村落,那么就把目标定为离该部落最近的村落,收复一个村落后把该点也加进来考虑下一个最近的村落,直至全部村落都在部落内(理解为修铁路也可以,将所有村落连起来)。
(1)选择起点
(2)从可移动的路径中找到最短路径
(3)判断此最短路径的两点是否已经在集合中,若在则删除此路径,不在则加入集合
(4)重复(2)(3)步骤直到所有点都已经在集合内,得出总路径
假设从a点开始,目前可移动的路径有7,4,那么选4,连接ac;
现在集合内拥有的点有a和c,可移动路径有7,6,8,9,选最小值6,连接cb;
重复执行,最后边的顺序为4,6,2,4,1,点的顺序为acbdfe;
#include#include #define INF 10000 using namespace std; const int N = 6;//6个点嘛 bool visit[N];//此点是否已在集合内 int dist[N] = {0};//当前集合离各个点的最小距离 int graph[N][N] = { {INF,7,4,INF,INF,INF}, //邻接矩阵,INF代表两点之间不可达 {7,INF,6,2,INF,4},//一维数组代表点,二维代表该点到其他点的距离 {4,6,INF,INF,9,8},//对角线为自身到自身,所以都是INF {INF,2,INF,INF,INF,7}, {INF,INF,9,INF,INF,1}, {INF,4,8,7,1,INF} }; int prim(int cur) { int index = cur;//起始点 int sum = 0;//总路径 int i = 0; int j = 0; cout << index << " ";//debug输出起始点 memset(visit,false, sizeof(visit));//所有点未在集合内 visit[cur] = true;//起点加入集合 for(i = 0; i < N; i++) dist[i] = graph[cur][i];//初始化,每个起点邻接的点的距离存入dist,这里存入 {INF,7,4,INF,INF,INF} for(i = 1; i < N; i++)//因为起点已经初始化过,故少遍历一次,i=1,而非i=0 { int minor = INF;//dist内的最小值 for(j = 0; j < N; j++) { if(!visit[j] && dist[j] < minor)//找到未访问的点中,距离当前最小生成树距离最小的点 { minor = dist[j];//更改最小值 index = j;//最小边所连接的点 } } visit[index] = true;//该点加入集合 cout << index << " ";//debug输出,该点 sum += minor;//总路程更新 for(j = 0; j < N; j++) { if(!visit[j] && dist[j]>graph[index][j])//执行更新,更新集合内到每个点的最小路径,对比一下新加入的点是否有更近路径,只判断集合外的点 { dist[j] = graph[index][j]; } } } cout << endl;//换行 return sum;//返回最小生成树的总路径值 } int main() { cout << prim(0) << endl;//从顶点a开始 return 0; }
graph[N][N]二维邻接矩阵代表点(一维)和点到每个点的距离(二维),
visit[N]代表是否在集合内,
dist[N]代表当前集合到各个点的最小距离,
总体流程为:
(1)初始化,将起点加入集合,visit=true,并获取其各个路径加入dist
(2)遍历N-1次(起点已经单独判断过,所以次数-1),在dist找到最短路径(只判断集合外的点,即!visit[j]),将最短的路径所连接的点加入集合,即visit=true。
(3)加入了新点后,获取该点到其他点的距离(只判断集合外的点,即!visit[j]),若有到其他点的更短路径,更新dist。
(4)重复(2)(3)直至全部点加入集合。
注释应该写的比较详细,结合代码和注释应该不难看懂。
2、KruskalPrim理解为集合从一个点开始慢慢吸纳其它的点进来。Kruskal则是以边为出发点,最短的路径先连起来,连接成好几个小集合再合并。举上文的例子就是,各个点分别为一个部落,其中距离相近的两个部落先进行合并,合并成了几个中等规模部落,然后这几个部落再按照距离近的进行合并。怪怪的,看下流程吧:
(1)每个点作为一个集合,将边的距离进行升序排序
(2)找到最短的路径,判断该边的两顶点是否在同一集合内,若在则跳过,将该边踢出检测队列,若不在则合并两集合
(3)重复(2)直到所有顶点都在同一集合内
注意图跟我放的链接文章不太一样,仍然为Prim的图,9条边。
第一步找到最短的边,长度为1,连接ef;
第二步找到最短的边,长度为2,连接bd;
第三步找到最短的边,长度为4,连接ac(bf也可,看排序细节了);
第四步找到最短的边,长度为4,连接bf;
注意:此时若de存在长度为5的边不会连接,因为在同一集合内,已经连同了!!!
第五步找到最短的边,长度为6,连接bc;
#include#include #include #define N 9 using namespace std; class Node{ public://class默认private int start; int end; int val; }; vector V(N);//边 int edge[N][3] = { {0,1,7},//start起点,end终点,val路径长度 {0,2,4}, {1,2,6}, {1,3,2}, {1,5,4}, {2,4,8}, {2,5,9}, {3,5,7}, {4,5,1} }; int father[N] = {0};//属于哪个集合,比如father[4]=0,4节点是属于0这个集合内的 int cap[N] = {0};//可以理解为集合优先级,也可以理解为吞并其它集合的次数,并非集合的元素个数,详细见下文 void make_set()//初始化集合,让所有的点都各成一个集合,每个集合都只包含自己 { for(int i = 0; i < N; i++) { father[i] = i;//每个点各自为一个集合 cap[i] = 1;//优先级都为1 } } int find_set(int x)//判断一个点属于哪个集合,点如果都有着共同的祖先结点,就可以说他们属于一个集合 { if(x != father[x])//如果点不在原始的集合,即已经加入了其它集合 { father[x] = find_set(father[x]);//获取该点所在的上级集合,上级若也并非原始集合,则递归找到最终的集合 } return father[x]; } void Union(int x, int y)//将x,y合并到同一个集合 { x = find_set(x);//找到最终集合 y = find_set(y); if(x == y)//同一个集合则返回,无需合并 return; if(cap[x] < cap[y])//优先级比他小,则归入他的集合 father[x] = y;//原来是find_set(y),但直接写y也一样 else { if(cap[x] == cap[y])//如果优先级相等 cap[x]++;//那么X的优先级增加,也可以理解成x吞并其它集合的次数,cap[x]不能理解为集合内元素的数量,不然此处应该为cap[x]+=cap[y]; father[y] = x;//y归入x ,x原为find_set(x) } } int Kruskal(int n) { int sum = 0; make_set(); for(int i = 0; i < N; i++)//将边的顺序按从小到大取出来 { if(find_set(V[i].start) != find_set(V[i].end))//如果边的两个顶点不在一个集合中,就并到一个集合里,生成树的长度加上这条边的长度 { Union(V[i].start, V[i].end); //合并两个顶点到一个集合 sum += V[i].val;//总路径增加 } } return sum; } int main() { for(int i = 0; i < N; i++)//初始化边的数据,在实际应用中可根据具体情况转换并且读取数据,这边只是测试用例 { V[i].start = edge[i][0]; V[i].end = edge[i][1]; V[i].val = edge[i][2]; } sort(V.begin(),V.end(),[](Node x,Node y)->bool{return x.val edge[N][3]:N指的是9条边,3是边的三个属性,左边的顶点和右边的顶点、长度(无向图,并非单向,两点反过来也一样)
father[N]:该点属于哪个集合,需要递归判断,比如a属于b,但b又属于c,也就是a其实是属于c
cap[N] :可以理解为集合优先级,也可以理解为吞并其它集合的次数,并非集合的元素个数
(1)初始化,依照边的长度进行升序排序。各个点分别为自己的集合
(2)排序后遍历每条边,若边两点不在一个集合内,则合并,若在一个集合内则不处理直接跳过
(3)重复执行(2)直到所有点都在同一集合内
注意:如果报错应该是匿名函数执行不了,这部分是C++11的新特性,老版本的编译器可能执行不了,需要在编译器上更改配置,或者直接不用匿名函数,按照下文进行修改即可
//sort(V.begin(),V.end(),[](Node x,Node y)->bool{return x.val



