北邮22信通一枚~
跟随课程进度每周更新数据结构与算法的代码和文章
持续关注作者 解锁更多邮苑信通专属代码~
获取更多文章 请访问专栏:
目录
一、最小生成树
在图论中,常常将树定义为一个无回路的连通图。连通图G的一个子图如果包含图G的所有顶点,则称该子图为图G的生成树。由于具有n个顶点的连通图至少有n-2条边,而包含n-1条边和n个顶点的连通图都是无回路的树,所以生成树还是连通图的极小连通子图。
什么是最小生成树呢?图的生成树不是唯一的,从不同顶点出发遍历可以得到不同的生成树,对于联通网G = (V,E),边是带权的,因而生成树的各边也带权。我们把生成树各边的权值总和成为生成树的权。我们把生成树各边的权值总和称为生成树的权,并吧权值最小对的生成树称为图G的最小生成树(Minimum Spanning Tree)。
生成树和最小生成树有很多重要的应用,例如,假设要在n个城市之间建立通信联络网,则连通n个城市只需要修剪n-1条线路,如何在最节省经费的前提下建立这个通信网?这个问题就可以转换为构造网的一颗最小生成树,即在e条带边权的边中选取n-1条(不构成回路)使权值之和为最小的问题。
下面介绍两种最小生成树的算法:Prim算法和Kruskal算法。
二、Prim算法
2.1、算法简介
一般情况下,假设n个顶点分成两个集合:U(包含已落在生成树上的点)和V-U(尚未落在生成树上的点),则在所有连通U中顶点和V-U中顶点的边种权值最小的边,就是最小生成树上的边。基于以上思想,Prim算法就是一个不断选取最小权值的边并把对应的顶点不断并入的过程。
下图是一个普利姆算法的实例,以粗线表示已经选取的最小生成树上的边和顶点,以虚线表示待选的边,以第一个顶点A作为起始的U集合,U->U-V表示所有集合U到集合V-U的边。
根据Prim算法的基本思想,Prim算法的完整实现可以分成三个部分:
2.3初始化辅助数据结构;
2.4选择辅助数组lowcost中的最小值;
2.5迭代辅助数组。
2.2、测试用图
该图信息用代码描述如下:
const int maxsize = 6;
const int MM = 99;//max_weight
//注:MM相当于断路,断路的权值可以取比图中最大权重还大的值作为权值
int adjvex[maxsize]; //U集中顶点的下标
int lowcost[maxsize]; //U->V-U的最小权值边
char ver[6] = { 'A','B','C','D','E','F' };
int arc[maxsize][maxsize] =
{
{0 ,34,46,MM,MM,19},
{34, 0,MM,MM,12,MM},
{46,MM, 0,17,MM,25},
{MM,MM,17, 0,38,25},
{MM,12,MM,38, 0,26},
{19,MM,25,25,26, 0}
};
2.3、存储结构
2.3.1、辅助数组
const int maxsize = 6;
int adjvex[maxsize]; //U集中顶点的下标
int lowcost[maxsize]; //U->V-U的最小权值边
初始化辅助数组:
int adjvex[maxsize]; //U集中顶点的下标
int lowcost[maxsize]; //U->V-U的最小权值边
template<class temp>
class graph
{
//……
};
template<class temp>
inline void graph<temp>::prim()
{
for (int i = 0; i < this->vnum; i++)
{
adjvex[i] = 0;
lowcost[i] = this->arc[0][i]; //初始时,辅助数组存储所有到v0的边。
}
lowcost[0] = 0;
//……
}
lowcost[0] = 0 表示该顶点已经选择;lowcost[3] = MM表示A->D没有直接连接的边;(注:MM相当于断路,断路的权值可以取比图中最大权重还大的值作为权值)lowcost[4] = MM表示A->E没有直接连接的边。
此时,辅助数组的数据如下:
下标 | 0 | 1 | 2 | 3 | 4 | 5 |
A | B | C | D | E | F | |
adjvex | 0 | 0 | 0 | 0 | 0 | 0 |
lowcost | 0 | 34 | 46 | MM | MM | 19 |
2.3.2、图类
Prim算法的存储结构是图类。
const int maxsize = 6;
const int MM = 99;//max_weight
template<class temp>
class graph
{
private:
temp vertex[maxsize];
int arc[maxsize][maxsize];
int vnum, arcnum;
public:
graph(ifstream& fin);
graph(temp a[], int arc[][maxsize], int vnum, int arcnum);
int mini_num();
void prim();
void prim(char start_node);
~graph();
};
template<class temp>
graph<temp>::graph(ifstream& fin)
{
fin >> this->vnum;
fin >> this->arcnum;
for (int i = 0; i < this->vnum; i++)
fin >> this->vertex[i];
for (int i = 0; i < this->vnum; i++)
for (int j = 0; j < this->vnum; j++)
fin >> this->arc[i][j];
}
template<class temp>
graph<temp>::graph(temp a[], int arc[][maxsize], int vnum, int arcnum)
{
this->vnum = vnum;
this->arcnum = arcnum;
for (int i = 0; i < this->vnum; i++)
this->vertex[i] = a[i];
for (int i = 0; i < this->vnum; i++)
for (int j = 0; j < this->vnum; j++)
this->arc[i][j] = arc[i][j];
}
2.4、辅助数组选最小值
选择辅助数组lowcost中的最小值,即U->V-U的权值边集合中最小的权值边。
const int maxsize = 6;
const int MM = 99;//max_weight
int adjvex[maxsize]; //U集中顶点的下标
int lowcost[maxsize]; //U->V-U的最小权值边
template<class temp>
class graph
{
//……
};
template<class temp>
inline int graph<temp>::mini_num()
{
int min_weight = MM;
int k = 0;
for (int i = 0; i < this->vnum; i++)
{
if (lowcost[i] != 0 && lowcost[i] < min_weight)
{
min_weight = lowcost[i];
k = i;
}
}
//cout << endl << "最小权值下标为" << k << endl;
return k;
}
3.5、迭代辅助数组
Prim算法中最关键的部分,就是迭代地更新辅助数据结构adjvex和lowcost。实际上,每增加一个新的顶点到集合U,U->V-U的边集就会更新一次,更新的原则就是寻找V-U的集合中的每一个顶点到U集合的最小权值。
通俗地讲,从A点开始,lowcost数组记录着U集到ABCDEF每个顶点的路程长短,选一个最短的,如图是F;
更新lowcost数组,加了F点,U集到ABCDEF更省力了吗?如果加了F,U集到ABCDEF的路程变短,那么就更新lowcost对应点的更短路程(比如U->C,U->D,U->E),并修改adjvex,因为加了F且F在顶点集中下标为5,使得U集到CDE更容易了,那么就在CDE对应的adjvex上标明,这是加了点F的功劳;
现在再从lowcost中选择最短路径,如图是C,更新lowcost数组,加了C点,U集到BCDE更省力吗?可以发现,原本U集->D需要25,现在加了C之后,U集->D可以变成17,这个功劳是加了C之后才有的,所以在D上修改adjvex为2……
依次类推,最终按顺序得到每次选出的点,即生成了最小生成树。
2.6、普利姆算法
2.6.1、辅助数组检测函数
void print_detect(int a[])
{
for (int i = 0; i < maxsize; i++)
cout << a[i] << " ";
cout << endl;
}
2.6.2、Prim算法代码
template<class temp>
class graph
{
//……
};
template<class temp>
inline void graph<temp>::prim()
{
for (int i = 0; i < this->vnum; i++)
{
adjvex[i] = 0;
lowcost[i] = this->arc[0][i]; //初始时,辅助数组存储所有到v0的边。
}
//print_detect(lowcost);
lowcost[0] = 0;
for (int i = 0; i < this->vnum - 1; i++)
{
//cout << endl << "第" << i << "次执行……" << endl;
int k = mini_num();
cout << "->" << this->vertex[k];
lowcost[k] = 0;
for (int j = 0; j < this->vnum; j++)
{
if (lowcost[j] != 0 &&
this->arc[k][j] < lowcost[j])
{
lowcost[j] = this->arc[k][j];
adjvex[j] = k;
}
}
print_detect(adjvex);
print_detect(lowcost);
}
}
int main()
{
system("color 0A");
try
{
graph<char>G(ver, arc, 6, 9);
//cout << "请问您准备从哪个基站开始遍历?" << endl;
//char input; cin >> input;
//G.prim(input);
G.prim();
return 0;
}
catch (const char* cc)
{
cout << cc;
}
}
2.6.3、运行结果
发现,结果和书中给出的循环中每次的辅助数组值完全相同。
2.7、从任意顶点的普利姆算法
若想从任意顶点生成最小生成树,需要改动的地方只有初始化lowcost。
template<class temp>
inline void graph<temp>::prim(char start_node)
{
int node_num = start_node - 'A';
if (node_num > vnum)throw"输入错误!";
for (int i = 0; i < this->vnum; i++)
{
adjvex[i] = 0;
lowcost[i] = this->arc[node_num][i]; //初始时,辅助数组存储所有到v0的边。
}
//print_detect(adjvex);
//print_detect(lowcost);
lowcost[node_num] = 0;
cout << this->vertex[node_num];
for (int i = 0; i < this->vnum - 1; i++)
{
//cout << endl << "第" << i << "次执行……" << endl;
int k = mini_num();
cout << "->" << this->vertex[k];
lowcost[k] = 0;
for (int j = 0; j < this->vnum; j++)
{
if (lowcost[j] != 0 &&
this->arc[k][j] < lowcost[j])
{
lowcost[j] = this->arc[k][j];
adjvex[j] = k;
}
}
//print_detect(adjvex);
//print_detect(lowcost);
}
}
2.8、整体代码及运行结果
#include<iostream>
#include<fstream>
using namespace std;
const int maxsize = 6;
const int MM = 99;//max_weight
int adjvex[maxsize]; //U集中顶点的下标
int lowcost[maxsize]; //U->V-U的最小权值边
char ver[6] = { 'A','B','C','D','E','F' };
int arc[maxsize][maxsize] =
{
{0 ,34,46,MM,MM,19},
{34, 0,MM,MM,12,MM},
{46,MM, 0,17,MM,25},
{MM,MM,17, 0,38,25},
{MM,12,MM,38, 0,26},
{19,MM,25,25,26, 0}
};
void print_detect(int a[])
{
for (int i = 0; i < maxsize; i++)
cout << a[i] << " ";
cout << endl;
}
template<class temp>
class graph
{
private:
temp vertex[maxsize];
int arc[maxsize][maxsize];
int vnum, arcnum;
public:
graph(ifstream& fin);
graph(temp a[], int arc[][maxsize], int vnum, int arcnum);
int mini_num();
void prim();
void prim(char start_node);
};
template<class temp>
graph<temp>::graph(ifstream& fin)
{
fin >> this->vnum;
fin >> this->arcnum;
for (int i = 0; i < this->vnum; i++)
fin >> this->vertex[i];
for (int i = 0; i < this->vnum; i++)
for (int j = 0; j < this->vnum; j++)
fin >> this->arc[i][j];
}
template<class temp>
graph<temp>::graph(temp a[], int arc[][maxsize], int vnum, int arcnum)
{
this->vnum = vnum;
this->arcnum = arcnum;
for (int i = 0; i < this->vnum; i++)
this->vertex[i] = a[i];
for (int i = 0; i < this->vnum; i++)
for (int j = 0; j < this->vnum; j++)
this->arc[i][j] = arc[i][j];
}
template<class temp>
inline int graph<temp>::mini_num()
{
int min_weight = MM;
int k = 0;
for (int i = 0; i < this->vnum; i++)
{
if (lowcost[i] != 0 && lowcost[i] < min_weight)
{
min_weight = lowcost[i];
k = i;
}
}
//cout << endl << "最小权值下标为" << k << endl;
return k;
}
template<class temp>
inline void graph<temp>::prim()
{
for (int i = 0; i < this->vnum; i++)
{
adjvex[i] = 0;
lowcost[i] = this->arc[0][i]; //初始时,辅助数组存储所有到v0的边。
}
//print_detect(lowcost);
lowcost[0] = 0;
for (int i = 0; i < this->vnum - 1; i++)
{
//cout << endl << "第" << i << "次执行……" << endl;
int k = mini_num();
cout << "->" << this->vertex[k];
lowcost[k] = 0;
for (int j = 0; j < this->vnum; j++)
{
if (lowcost[j] != 0 &&
this->arc[k][j] < lowcost[j])
{
lowcost[j] = this->arc[k][j];
adjvex[j] = k;
}
}
//print_detect(adjvex);
//print_detect(lowcost);
}
}
template<class temp>
inline void graph<temp>::prim(char start_node)
{
int node_num = start_node - 'A';
if (node_num > vnum)throw"输入错误!";
for (int i = 0; i < this->vnum; i++)
{
adjvex[i] = 0;
lowcost[i] = this->arc[node_num][i]; //初始时,辅助数组存储所有到v0的边。
}
//print_detect(adjvex);
//print_detect(lowcost);
lowcost[node_num] = 0;
cout << this->vertex[node_num];
for (int i = 0; i < this->vnum - 1; i++)
{
//cout << endl << "第" << i << "次执行……" << endl;
int k = mini_num();
cout << "->" << this->vertex[k];
lowcost[k] = 0;
for (int j = 0; j < this->vnum; j++)
{
if (lowcost[j] != 0 &&
this->arc[k][j] < lowcost[j])
{
lowcost[j] = this->arc[k][j];
adjvex[j] = k;
}
}
//print_detect(adjvex);
//print_detect(lowcost);
}
cout << endl;
}
int main()
{
system("color 0A");
while(1)
{
try
{
graph<char>G(ver, arc, 6, 9);
cout << "请问您准备从哪个基站开始遍历?" << endl;
char input; cin >> input;
G.prim(input);
//G.prim();
}
catch (const char* cc)
{
cout << cc;
}
}
return 0;
}
三、Kruskal算法
3.1、算法简介
另一种最小生成树算法是克鲁斯卡尔算法,如前所述,Prim算法是从顶点的角度,通过依次增加不同的顶点,完成生成树的构造。而克鲁斯卡尔算法则是从边的角度,通过依次增加不同的边,来完成生成树的构造。
克鲁斯卡尔算法的基本思想是,为使生成树边的权值之和最小,显然,从中每一条边的权值应该尽可能的小。因此,克鲁斯卡尔算法首先对边进行从小到大的排序,然后从权值最小的边开始,依次添加到生成树当中,直至添加到第n-1条边为止。当然,不是所有的权值较小的边都是有效的,因此,每添加一条新的边到生成树上,必须保证新添加的边不会使已有的生成树产生回路。
克鲁斯卡尔算法主要分为两个部分,一个是对边进行排序,另一部分是添加一条不产生回路的最小边。由于克鲁斯卡尔算法要对边进行排序,所以要选择一个辅助数组,即边集数组作为图的存储结构,以方便对图中所有的边按权值从小到大进行排序。
3.2、测试用图
该图的信息用代码描述如下:
const int max_node = 7; //图有多少个顶点
const int MM = 99; //max_weight;
//注:MM相当于断路,断路的权值可以取比图中最大权重还大的值作为权值
const int max_edge = 11; //图有多少条边
int arc[max_node][max_node] =
{
{ 0,19,MM,MM,14,MM,18},
{19, 0, 5, 7,12,MM,MM},
{MM, 5, 0, 3,MM,MM,MM},
{MM, 7, 3, 0, 8,21,MM},
{14,12,MM, 8, 0,MM,16},
{MM,MM,MM,21,MM, 0,27},
{18,MM,MM,MM,16,27, 0}
};
char node_name[max_node] = { 'a','b','c','d','e','f','g' };
3.3、存储结构
3.3.1、边集数组
边集数组由图的邻接表或邻接矩阵构成,其数据结构用代码表示:
const int max_edge = 11;
struct v_edge
{
int from_v;
int end_v;
int weight;
};
v_edge edge_list[max_edge];
3.3.2、图类
图由构造函数、析构函数、边权排序函数和Kruskal算法函数构成。
const int max_node = 7;
const int MM = 99;//max_weight;
const int max_edge = 11;
int arc[max_node][max_node] =
{
{ 0,19,MM,MM,14,MM,18},
{19, 0, 5, 7,12,MM,MM},
{MM, 5, 0, 3,MM,MM,MM},
{MM, 7, 3, 0, 8,21,MM},
{14,12,MM, 8, 0,MM,16},
{MM,MM,MM,21,MM, 0,27},
{18,MM,MM,MM,16,27, 0}
};
char node_name[max_node] = { 'a','b','c','d','e','f','g' };
template<class temp>
class graph
{
private:
temp vertex[max_node];
int arc[max_node][max_node];
int vnum, arcnum;
public:
graph(ifstream& fin);
graph(temp a[], int arc[][max_node], int vnum, int arcnum);
void gen_short_edge(); //边权排序
void kruskal(); //Kruskal算法
~graph();
};
template<class temp>
graph<temp>::graph(ifstream& fin)
{
fin >> this->vnum;
fin >> this->arcnum;
for (int i = 0; i < this->vnum; i++)
fin >> this->vertex[i];
for (int i = 0; i < this->vnum; i++)
for (int j = 0; j < this->vnum; j++)
fin >> this->arc[i][j];
}
template<class temp>
graph<temp>::graph(temp a[], int arc[][max_node], int vnum, int arcnum)
{
this->vnum = vnum;
this->arcnum = arcnum;
for (int i = 0; i < this->vnum; i++)
this->vertex[i] = a[i];
for (int i = 0; i < this->vnum; i++)
for (int j = 0; j < this->vnum; j++)
this->arc[i][j] = arc[i][j];
}
3.3.3、辅助数组
克鲁斯卡尔算法还需要一个辅助数组int vset[max_node],用来判断新加入的边是否构成回路。
const int max_node = 7;
int vset[max_node];
3.4、边权排序
首先获取edge_list,并对其进行排序,用C++描述如下:
template<class temp>
inline void graph<temp>::gen_short_edge()
{
int k = 0;
for (int i = 0; i < this->vnum; i++) //对边进行赋值
for (int j = i + 1; j < this->vnum; j++)
if (this->arc[i][j] != MM)
{
edge_list[k].from_v = i;
edge_list[k].end_v = j;
edge_list[k].weight = this->arc[i][j];
k++;
}
for (int i = 0; i < this->arcnum - 1; i++) //对边进行排序,此处选用冒泡排序算法
{
for(int j = i+1;j<this->arcnum;j++)
if (edge_list[i].weight > edge_list[j].weight)
{
v_edge t = edge_list[i];
edge_list[i] = edge_list[j];
edge_list[j] = t;
}
}
//detect_edge_list();
}
3.5、迭代辅助数组
克鲁斯卡尔算法还需要一个辅助数组int vset[max_node],用来判断新加入的边是否构成回路。判断构成回路的原则是:所有连通的点属于同一个集合,不能连通的顶点分别属于不同的集合,每添加一条新的边时,需要判断该边对应的两个顶点是否同属一个集合,若两个顶点属于不同的集合,则不会构成回路,可以添加,否则选择下一条边进行判断。
辅助数组int vset[max_node]的初始值如下表表示。期中,vset数组的每一个元素对应一个顶点,初始化时每个顶点分属不同的集合,vset的值表示集合的编号。
a | b | c | d | e | f | g | |
vset | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
因此,从小到大添加边的过程就是:若其中一个顶点所属集合的编号为i,另一个顶点所属集合编号为j,则在vset中寻找所有编号为j的顶点,将其更新为i。以下图为例,添加边cd、bc和de的过程中vset数组的数据更新过程如下图所示。
3.6、克鲁斯卡尔算法
首先初始化辅助数组vset。
当输出的顶点数少于图总共的顶点数时:
取第一个按边权从小到大排好序的边权数组edge_list的第一个边元素,将该边的一端顶点归为m集合,另一端顶点归为n集合。
怎么判断两个集合是不是不属于同一个结合呢?因为vset数组的每个索引的值代表不同的集合标号,所以如果vset[m]对应的值和vset[n]对应的值不同,就说明两个顶点不属于同一个集合。
如果两个顶点不属于同一个集合,则说明该边可选入最小生成树路径。
遍历vset数组,如果发现某顶点和新选入边的某顶点原先属于另一个集合,则将这个顶点加入到已选顶点集合中。
template<class temp>
inline void graph<temp>::kruskal()
{
//初始化vset
for (int i = 0; i < this->vnum; i++)
vset[i] = i;
int k = 0, j = 0; //k是用来统计输出了几个顶点的
//j是用来遍历边权数组的
while (k < this->vnum - 1)
{
int m = edge_list[j].from_v;
int n = edge_list[j].end_v;
int sn1 = vset[m]; //m所属集合
int sn2 = vset[n]; //n所属集合
//m小n大
if (sn2 != sn1) //两个顶点不属于同一个集合
{
k++; //k是用来统计输出了几个顶点的
for(int i=0;i<this->vnum;i++)
if (vset[i] == sn2)
vset[i] = sn1;
//遍历辅助数组,将所有存在于n集合的元素归并到m集合中
}
j++;//边权数组运行下一组数据
}
}
3.7、书上代码及运行结果
3.7.1、边权数组的排序检测
void detect_edge_list()
{
cout << endl << "start_edge_list_detect……" << endl;
for (int i = 0; i < max_edge; i++)
cout << "from_v:" << edge_list[i].from_v
<< "end_v:" << edge_list[i].end_v
<< "weight:" << edge_list[i].weight << endl;
cout << "edge_list_detect_over!" << endl;
}
class graph
{
//……
};
template<class temp>
inline void graph<temp>::gen_short_edge()
{
int k = 0;
for (int i = 0; i < this->vnum; i++)
for (int j = i + 1; j < this->vnum; j++)
if (this->arc[i][j] != MM)
{
edge_list[k].from_v = i;
edge_list[k].end_v = j;
edge_list[k].weight = this->arc[i][j];
k++;
}
for (int i = 0; i < this->arcnum - 1; i++)
{
for(int j = i+1;j<this->arcnum;j++)
if (edge_list[i].weight > edge_list[j].weight)
{
v_edge t = edge_list[i];
edge_list[i] = edge_list[j];
edge_list[j] = t;
}
}
detect_edge_list();
}
int main()
{
system("color 0A");
graph<char>G(node_name, arc, max_node, max_edge);
G.gen_short_edge();
G.kruskal();
return 0;
}
检测结果:
结论:
排序正确。
3.7.2、克鲁斯卡尔算法检测
void vset_detect()
{
cout << endl << "start_vset_detect……" << endl;
for (int i = 0; i < max_node; i++)
cout << vset[i] << " ";
cout << endl << "vset_detect_over!" << endl;
}
template<class temp>
class graph
{
//……
};
template<class temp>
inline void graph<temp>::kruskal()
{
//初始化vset
for (int i = 0; i < this->vnum; i++)
vset[i] = i;
int k = 0, j = 0;
while (k < this->vnum - 1)
{
cout << endl << "第" << k << "次运行……" << endl;//加入检测
int m = edge_list[j].from_v;
int n = edge_list[j].end_v;
int sn1 = vset[m];
int sn2 = vset[n];
//m小n大
if (sn2 != sn1)
{
k++;
cout << "选边成功!" << endl; //加入检测
for (int i = 0; i < this->vnum; i++)
if (vset[i] == sn2)
vset[i] = sn1;
}
else cout << "成环!" << endl; //假如检测
cout << node_name[m] << node_name[n] << endl; //假如检测
vset_detect(); //加入检测
j++;
}
}
int main()
{
system("color 0A");
graph<char>G(node_name, arc, max_node, max_edge);
G.gen_short_edge();
G.kruskal();
return 0;
}
检测结果:
结论:辅助数组和书上有出入,但选边正确。
3.8、一个错误并更正
3.8.1、前后代码对比
为了改正辅助数组,使之和书上的辅助数组输出一致,并最终能给出选出顶点的顺序,现对Kruskal算法函数做如下改动:
原代码:
template<class temp>
inline void graph<temp>::kruskal()
{
//初始化vset
for (int i = 0; i < this->vnum; i++)
vset[i] = i;
int k = 0, j = 0;
while (k < this->vnum - 1)
{
//cout << endl << "第" << k << "次运行……" << endl;
int m = edge_list[j].from_v;
int n = edge_list[j].end_v;
int sn1 = vset[m];
int sn2 = vset[n];
//m小n大
if (sn2 != sn1)
{
k++;
//cout << "选边成功!" << endl;
for (int i = 0; i < this->vnum; i++)
if (vset[i] == sn2)
vset[i] = sn1;
}
//else cout << "成环!" << endl;
//cout << node_name[m] << node_name[n] << endl;
//vset_detect();
j++;
}
}
改动后:
bool flag[max_node];
template<class temp>
inline void graph<temp>::kruskal()
{
//初始化vset
for (int i = 0; i < this->vnum; i++)
vset[i] = i;
int k = 0, j = 0;
while (k < this->vnum - 1)
{
//cout << endl << "第" << k << "次运行……" << endl;
int m = edge_list[j].from_v;
int n = edge_list[j].end_v;
int sn1 = vset[m];
int sn2 = vset[n];
//m小n大
//else cout << "成环!" << endl;
//cout << node_name[m] << node_name[n] << endl;
if (sn2 != sn1)
{
k++;
for (int i = 0; i < this->vnum; i++)
{
if (flag[sn1] == 0)
{
if (vset[i] == sn2)
vset[i] = sn1;
}
else
{
if (vset[i] == sn1)
vset[i] = sn2;
}
}
if (flag[sn1] == 0)
{
flag[sn1] = 1;
cout << this->vertex[sn1] << " ";
if (flag[sn2] == 0)
cout << this->vertex[sn2] << " ";
}
else
{
flag[sn2] = 1;
cout << this->vertex[sn2] << " ";
}
vset_detect();
//注意!!!调了detect的位置!!!
}
j++;
}
}
3.8.2、 改动思路
我们发现,书上给出的表格:
a | b | c | d | e | f | g | |
vset | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
a | b | c | d | e | f | g | |
vset | 0 | 1 | 2 | 2 | 4 | 5 | 6 |
a | b | c | d | e | f | g | |
vset | 0 | 1 | 1 | 1 | 4 | 5 | 6 |
a | b | c | d | e | f | g | |
vset | 0 | 4 | 4 | 4 | 4 | 5 | 6 |
而原代码运行后:
a | b | c | d | e | f | g | |
vset | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
a | b | c | d | e | f | g | |
vset | 0 | 1 | 2 | 2 | 4 | 5 | 6 |
a | b | c | d | e | f | g | |
vset | 0 | 1 | 1 | 1 | 4 | 5 | 6 |
a | b | c | d | e | f | g | |
vset | 0 | 1 | 1 | 1 | 1 | 5 | 6 |
观察规律得:书上给的表格,每次新增一条边,都要用新增边中新增的顶点来重命名“已选顶点集合”,而书上给出的代码运行后,实验现象却是每新增一条边,这条边的所有顶点都将被重命名为“1”。
所以,我们可以设置一个bool类型的flag数组,大小就是图中顶点个数,初始值全为假。若某个顶点已经选过,则变更其flag属性为1。每次对vset重命名时,需选择flag属性为0的顶点来命名,即:
int m = edge_list[j].from_v;
int n = edge_list[j].end_v;
int sn1 = vset[m];
int sn2 = vset[n];
if (sn2 != sn1)
{
k++;
for (int i = 0; i < this->vnum; i++)
{
if (flag[sn1] == 0)
//如果sn1的flag是0,则将vset中所有和sn2在同一个集合中的顶点重命名为sn1
{
if (vset[i] == sn2)
vset[i] = sn1;
}
else
//如果sn1的flag是1,则将vset中所有和sn1在同一个集合中的顶点重命名为sn2
{
if (vset[i] == sn1)
vset[i] = sn2;
}
}
if (flag[sn1] == 0)
//如果sn1的flag是0,变更sn1的flag为1,同时输出vertex[sn1]
//如果sn1的flag是0的同时sn2的flag也为0:
//这种情况只出现在选第一条边时;
//输出vertex[sn2]。
{
flag[sn1] = 1;
cout << this->vertex[sn1] << " ";
if (flag[sn2] == 0)
cout << this->vertex[sn2] << " ";
}
else
//如果sn1的flag是1,则sn2的flag必为0,
//现在变sn2的flag为1,同时输出vertex[sn2]
{
flag[sn2] = 1;
cout << this->vertex[sn2] << " ";
}
}
3.8.3、改动后运行结果
注意:为了保证简洁性,我们将vset_detect()函数放在了 if(sn1 != sn2)中,保证只有选路成功才会detect一次,成环时避免了无效输出,输出结果如下:
3.9、整体代码
#include <iostream>
#include <fstream>
using namespace std;
const int max_node = 7;
const int MM = 99;//max_weight;
const int max_edge = 11;
int arc[max_node][max_node] =
{
{ 0,19,MM,MM,14,MM,18},
{19, 0, 5, 7,12,MM,MM},
{MM, 5, 0, 3,MM,MM,MM},
{MM, 7, 3, 0, 8,21,MM},
{14,12,MM, 8, 0,MM,16},
{MM,MM,MM,21,MM, 0,27},
{18,MM,MM,MM,16,27, 0}
};
char node_name[max_node] = { 'a','b','c','d','e','f','g' };
struct v_edge
{
int from_v;
int end_v;
int weight;
};
v_edge edge_list[max_edge];
int vset[max_node];
bool flag[max_node];
void detect_edge_list()
{
cout << endl << "start_edge_list_detect……" << endl;
for (int i = 0; i < max_edge; i++)
cout << "from_v:" << edge_list[i].from_v
<< "end_v:" << edge_list[i].end_v
<< "weight:" << edge_list[i].weight << endl;
cout << "edge_list_detect_over!" << endl;
}
void vset_detect()
{
cout << endl << "start_vset_detect……" << endl;
for (int i = 0; i < max_node; i++)
cout << vset[i] << " ";
cout << endl << "vset_detect_over!" << endl;
}
template<class temp>
class graph
{
private:
temp vertex[max_node];
int arc[max_node][max_node];
int vnum, arcnum;
public:
graph(ifstream& fin);
graph(temp a[], int arc[][max_node], int vnum, int arcnum);
void gen_short_edge();
void kruskal();
};
template<class temp>
graph<temp>::graph(ifstream& fin)
{
fin >> this->vnum;
fin >> this->arcnum;
for (int i = 0; i < this->vnum; i++)
fin >> this->vertex[i];
for (int i = 0; i < this->vnum; i++)
for (int j = 0; j < this->vnum; j++)
fin >> this->arc[i][j];
}
template<class temp>
graph<temp>::graph(temp a[], int arc[][max_node], int vnum, int arcnum)
{
this->vnum = vnum;
this->arcnum = arcnum;
for (int i = 0; i < this->vnum; i++)
this->vertex[i] = a[i];
for (int i = 0; i < this->vnum; i++)
for (int j = 0; j < this->vnum; j++)
this->arc[i][j] = arc[i][j];
}
template<class temp>
inline void graph<temp>::gen_short_edge()
{
int k = 0;
for (int i = 0; i < this->vnum; i++)
for (int j = i + 1; j < this->vnum; j++)
if (this->arc[i][j] != MM)
{
edge_list[k].from_v = i;
edge_list[k].end_v = j;
edge_list[k].weight = this->arc[i][j];
k++;
}
for (int i = 0; i < this->arcnum - 1; i++)
{
for(int j = i+1;j<this->arcnum;j++)
if (edge_list[i].weight > edge_list[j].weight)
{
v_edge t = edge_list[i];
edge_list[i] = edge_list[j];
edge_list[j] = t;
}
}
detect_edge_list();
}
template<class temp>
inline void graph<temp>::kruskal()
{
//初始化vset
for (int i = 0; i < this->vnum; i++)
vset[i] = i;
int k = 0, j = 0;
while (k < this->vnum - 1)
{
//cout << endl << "第" << k << "次运行……" << endl;
int m = edge_list[j].from_v;
int n = edge_list[j].end_v;
int sn1 = vset[m];
int sn2 = vset[n];
//m小n大
//else cout << "成环!" << endl;
//cout << node_name[m] << node_name[n] << endl;
if (sn2 != sn1)
{
k++;
for (int i = 0; i < this->vnum; i++)
{
if (flag[sn1] == 0)
{
if (vset[i] == sn2)
vset[i] = sn1;
}
else
{
if (vset[i] == sn1)
vset[i] = sn2;
}
}
if (flag[sn1] == 0)
{
flag[sn1] = 1;
cout << this->vertex[sn1] << " ";
if (flag[sn2] == 0)
cout << this->vertex[sn2] << " ";
}
else
{
flag[sn2] = 1;
cout << this->vertex[sn2] << " ";
}
vset_detect();
}
j++;
}
}
int main()
{
system("color 0A");
graph<char>G(node_name, arc, max_node, max_edge);
G.gen_short_edge();
G.kruskal();
return 0;
}
3.10、对输出的思考
3.10.1、flag能去掉吗?
如果只是输出选边顺序,则flag可以不存在,但如果想输出精确的选点顺序,则flag必须存在。
3.10.2、可以简化吗?
实际运用中,我们不必输出vset数组,也不必使用新顶点来更新“已选顶点集合”。其实vset的顶点集合就分为两类,一类是选边之后发现已经选了的,一种是还没有被选中的。
所以vset数组其实可以不用改变,只用bool的真假分别代表已选和未选。
这样,对Kruskal算法的改进应写为:
template<class temp>
inline void graph<temp>::kruskal_improved()
{
//初始化vset
for (int i = 0; i < this->vnum; i++)
vset[i] = i;
int k = 0, j = 0;
while (k != this->vnum)
{
int m = edge_list[j].from_v;
int n = edge_list[j].end_v;
int sn1 = vset[m];
int sn2 = vset[n];
if (sn2 != sn1)
{
for (int i = 0; i < this->vnum; i++)
{
if (vset[i] == sn1 && flag[vset[i]] == 0)
{
cout << this->vertex[sn1] << " "; k++;
flag[vset[i]] = 1;
}
if (vset[i] == sn2 && flag[vset[i]] == 0)
{
cout << this->vertex[sn2] << " "; k++;
flag[vset[i]] = 1;
}
}
}
j++;
}
}