嘿嘿嘿,小伙伴们,今天咱们来咬文嚼字,看看这最小生成树是个啥玩意
有趣的问答
Q1:树是啥?
A1:树是连通无回路的图
Q2: 连通我知道,就是任意两个点均可达,类似于向一个点注水,整个图都有水喝;无回路嘛,应该就是不存在圈。是这样叭??
A2:真是个小机灵鬼儿哟,想不到你的理解已经如此深刻,其实呢,它还有一个隐含特性:边数比节点数少一,所以任意图想变成树,就得先控制边数,也就是毛线数只能为8-1,也就是7,他才有可能变成树哟。
Q3:那生成树是啥?在树的基础上添加限制条件??
A3: 没错,你可真聪明,生成树嘞,可以这么看,假设一个图有8个节点,把所有的节点用乒乓球表示,若是图中两个点有线相接,那就用毛线把相应的乒乓球连起来,这个乒乓球版的图就做好啦。我们再找找生成的主语和宾语,大家习惯这么说:图 生成 树,所以图为主语,树为宾语,这其实是一个动态过程,由图转化为树,生成的含义也暗示我们树必定是图的一部分,不存在树比图多点,多边情况,然后又有大佬规定生成树的节点个数等于图的节点个数,然后树枝只能从图中已有边取。所以生成树就是在图上选出来的一棵树。若是想检验自己是否选对了生成树,就把乒乓球版图中选中的毛线留下,其余毛线全减咯,然后抓住一颗球拉倒空中,没有球落地且每一根毛线都被扯直了,那么,恭喜你,选对了
Q4:哦哦,原来如此呀,那最小生成树是不是所有生成树中边的和最短的?
A4:完全正确
Q5:若我要寻找一个有n个顶点的图的最小生成树,其实树的顶点已经确定,就是图的n个顶点;然后再选择n-1条边,只要这n-1条边和n个点是连通无回路的,那么意味着我找到了一颗生成树。只要我找出所有生成树,通过比较他们的边权之和,选出最小的那个就是最小生成树啦?
A5:小伙子,你可真是个学离散的料,这个思路是可行的,不过若是边比较多,那你的工作量就很大了
Q6:那怎么办?没有简便方法吗?
A6:嘿嘿,有滴,感谢前辈们的智慧,创造了许多方法,我给你讲讲最近刚学会的避圈法,它的英文名是Kruskal,不过还是中文比较贴切,避圈吗,就是逃避圈呗,接下来具体介绍它,come on baby~
核心–避圈
之所以给他起了个中文名–避圈法,就是它的算法特点–躲着圈。
它的思想很简单,先把图中所有点拎出来放在桌面上,边一条不拎(光秃秃的真可怜)。其次将边按权值从小到大排个序,放到一个只出不进的箱子(真大方)里。接着每次从只出不进的箱子取出一条最小边,若是该边的两个顶点在桌面上已连通,就丢了这条边(不许放回箱子);若是两个顶点在桌面上八竿子打不着,就用这条边将桌面上两点连起来。只要向桌面加入(节点数 - 1)条边,恭喜你,大功告成咯
从它的思想描述中可以抽出两个关键步骤:
实现思路
数据结构
typedef struct{
int n,m;//定点数,边数
}Graph;//图
typedef struct{
int u,v,w;//起止点,权值
bool MST;//判断是否被选为最小生成树的树枝
}Edge;//边结构
Graph G;//全局定义,图
Edge* edge;//边集,动态数组
先啃并查集
并查集核心在于并(合并),查(查找),初始化也很重要
详细可参考上文链接,此文重心在于求最小生成树
查找
//查找child所在子树的根节点,并压缩路径,即将该子树中所有的点直接指向根
int Find(int child,int* parent)
{
//查找所在子树的根节点
int f = child;
while(parent[f] > 0){
f = parent[f];
}
//压缩路径
int j = child;
while(j != f){
parent[j] = f;
j = parent[j];
}
return f;
}
合并
//合并两个子树的根
void Union(int a,int b,int* parent)
{
//权值小的做根,优化
if(edge[a].w > edge[b].w){
parent[a] += parent[b];
parent[b] = a;
}
else{
parent[b] += parent[a];
parent[a] = b;
}
}
边排序
对关键字:边权,进行简单选择排序
//根据边权升序排列:简单选择排序
void SortEdge()
{
for(int i = 0; i < G.m; i++){
int k = i;//cout<<i<<endl;
for(int j = i+1; j < G.m; j++){
if(edge[k].w > edge[j].w){
k = j;
}
}
if(k != i){
Swap(edge[k],edge[i]);
}
}
TraverseEdge();//打印调试
}
集成
前边难点已经被攻克,零件以准备好,这里只需要组装即可
结合注释很容易的(偷个懒~)
//克鲁斯卡尔算法
void Kruskal()
{
InitEdge();//初始化边
SortEdge();//边升序排列
int* parent;//并查集准备:记录当前点对应的双亲节点
parent = (int*)malloc(G.n*sizeof(int));
for(int i = 1; i <= G.n; i++){//从下标1开始,初始化为-1
parent[i] = -1;
}
int j = 0;//控制边的下标:第j条边
for(int i = 0; i < G.n-1; i++){//选取n-1条边
while(true){TraverseEdge();
int u,v;
u = edge[j].u;
v = edge[j].v;
int fa,fb;
fa = Find(u,parent);//寻找u,v的根节点
fb = Find(v,parent);
if(fa == fb)j++;//u,v在同一连通分支,选择下一条边
else{//不连通
Union(fa,fb,parent);
edge[j].MST = true;//表示被选中
j++;//下一条边
break;//跳出,进入下一条边的选择
}
}
}
cout<<endl<<"======最小生成树======"<<endl;
for(int i = 0; i < G.m; i++){
if(edge[i].MST){
cout<<"u:"<<edge[i].u<<" v:"<<edge[i].v<<" w:"<<edge[i].w<<endl;
}
}
}
小收获
- 不惧挑战:以前一直以为克鲁斯卡尔算法很难,因为老师在课上说这个不好写,直接掌握Prim就好,这就把我给劝退了,直到昨天写数构课设时有一道求解朋友圈的题目,一开始真不会,上文求索后,原来关键在于一个并查集呀,它也是种数据结构,只闻其名,不见其人,一直以为他是很高深的算法呢,原来简单的不得了,学会它后自然要触类旁通,顺便解决下Kruskal这个心魔,嘿嘿嘿写出来还是比较顺利的呀
- 最近事情比较多,感觉有些浮躁,学习时无法静心凝神,深入问题本质,希望尽快调整心态,勿忘初衷,什么都别想太多,小伙子,人生路很长,你好好走就是~共勉之
- 以后每天背一首诗,天天在计算机的世界里都快失去感性咯
- 生活要慢,做事要快,两者分得开些,焦虑少一些
完整Code
从文件读入的数据
文件最小生成树.txt内容(第一行为顶点,边个数;其余行每三个数表示点顶点u,顶点v,权值w)
6 10
0 1 6 0 2 1 0 3 5
1 2 5 1 4 3
2 3 5 2 4 6 2 5 4
3 5 2
4 5 6
结果
#include<iostream>
using namespace std;
#include<stdlib.h>
#include<fstream>
typedef struct{
int n,m;//定点数,边数
}Graph;
typedef struct{
int u,v,w;//起止点,权值
bool MST;//判断是否被选为最小生成树的树枝
}Edge;//边结构
Graph G;//全局定义,图
Edge* edge;//边集,动态数组
//交换两条边
void Swap(Edge &e1,Edge &e2)
{
int t;
t = e1.u;e1.u = e2.u;e2.u = t;
t = e1.v;e1.v = e2.v;e2.v = t;
t = e1.w;e1.w = e2.w;e2.w = t;
}
//打印调试用的
void TraverseEdge()
{
for(int i = 0; i < G.m; i++){
cout<<edge[i].u<<" "<<edge[i].v<<" "<<edge[i].w<<endl;
}
}
//根据边权升序排列:简单选择排序
void SortEdge()
{
for(int i = 0; i < G.m; i++){
int k = i;cout<<i<<endl;
for(int j = i+1; j < G.m; j++){
if(edge[k].w > edge[j].w){
k = j;
}
}
if(k != i){
Swap(edge[k],edge[i]);
}
}
TraverseEdge();
}
void InitEdge()
{
fstream inFile("最小生成树.txt",ios::in);
if(!inFile)cout<<"fail to open file!"<<endl;
inFile>>G.n>>G.m;
edge = (Edge*)malloc(G.m*sizeof(Edge));
for(int i = 0; i < G.m; i++){
inFile>>edge[i].u>>edge[i].v>>edge[i].w;
edge[i].u += 1;//输入的点从0开始,存储时从1开始
edge[i].v += 1;
edge[i].MST = false;
}
// TraverseEdge();
inFile.close();
}
//查找child所在子树的根节点,并压缩路径,即将该子树中所有的点直接指向根
int Find(int child,int* parent)
{
//查找所在子树的根节点
int f = child;
while(parent[f] > 0){
f = parent[f];
}
//压缩路径
int j = child;
while(j != f){
parent[j] = f;
j = parent[j];
}
return f;
}
//合并两个子树的根
void Union(int a,int b,int* parent)
{
//权值小的做根,优化
if(edge[a].w > edge[b].w){
parent[a] += parent[b];
parent[b] = a;
}
else{
parent[b] += parent[a];
parent[a] = b;
}
}
//克鲁斯卡尔算法
void Kruskal()
{
InitEdge();//初始化边
SortEdge();//边升序排列
int* parent;//并查集准备:记录当前点对应的双亲节点
parent = (int*)malloc(G.n*sizeof(int));
for(int i = 1; i <= G.n; i++){//从下标1开始,初始化为-1
parent[i] = -1;
}
int j = 0;//控制边的下标:第j条边
for(int i = 0; i < G.n-1; i++){//选取n-1条边
while(true){TraverseEdge();
int u,v;
u = edge[j].u;
v = edge[j].v;
int fa,fb;
fa = Find(u,parent);//寻找u,v的根节点
fb = Find(v,parent);
if(fa == fb)j++;//u,v在同一连通分支,选择下一条边
else{//不连通
Union(fa,fb,parent);
edge[j].MST = true;//表示被选中
j++;//下一条边
break;//跳出,进入下一条边的选择
}
}
}
cout<<endl<<"======最小生成树======"<<endl;
for(int i = 0; i < G.m; i++){
if(edge[i].MST){
cout<<"u:"<<edge[i].u<<" v:"<<edge[i].v<<" w:"<<edge[i].w<<endl;
}
}
}
int main()
{
Kruskal();
return 0;
}