一、生成树和最小生成树的概念:
生成树:只存在无向图中,即极小连通子图。简单来说就是在无向连通图中含有n个点,使用n-1条边即可把这n个点全部连通。
生成树的属性:1,一个连通图中包含多棵生成树;2,每颗生成树的顶点数和边数一定相同;3,生成树中不存在环;4,生成树中失去一条边就不能构成生成树;5,生成树中任意加入一条边就会形成环;6,n个顶点的连通图中,构成的生成树含有n个顶点和n-1条边;7,包含n个顶点的无向完全图中最多含有n^(n-2)棵生成树。
最小生成树:在m条边中,选n-1条边使得图中的n个顶点连通并且这n-1条边的边权之和最小。
二、Kruskal算法:
Kruskal算法是以选边为中心,最终实现构成最小生成树的算法。核心思想是:贪心+排序+并查集。
选边:每次选择边权最小的边即贪心思想,利用排序算法每次选择最小的,在选边时还要注意在选该边时必须保证该边的俩个端点处于非连通状态。
判断是否连通:利用并查集查看他们是否处于同一个“集合”中,且用路径压缩优化并查集。
//Kruskal算法
//以选边为主。每次选择一条权值最小的边且这条边在选之前的点要是非连通状态
//贪心+排序+并查集
//因为涉及到排序对边权,就采用边集数组,并且使下标编号就是该数据本身
#include <iostream>
typedef struct Edge {
int u, v;//无向图边的俩个端点
int w;//边权
}Edge;
Edge e[105];//存的是边的信息
int n, m, w;
int tree[105];//并查集
void sortt(int l, int r)//对边的边权进行排序
{
int minn;//记录的是最小边权的那条边的位置
Edge tmp;
for (int i = l; i < r; ++i) //一共只需要循环r-l次
{
minn = i;
for (int j = i + 1; j <= r; ++j)
{
if (e[minn].w > e[j].w)
{
minn = j;
}
}
tmp = e[minn];
e[minn] = e[i];
e[i] = tmp;
}
}
int find(int x)
{
return tree[x] = (tree[x] == x ? x : find(tree[x]));//路径压缩
}
void kruskal()
{
for (int i = 0; i < n; ++i)
{
tree[i] = i;//先令自己的根节点为本身
}
int cnt = 0, sum = 0;
int fu, fv;
for (int i = 1; i <= m; ++i) //因为进行过排序,所以所有的边是按边权递增的
{
fu = find(e[i].u);//找数据的根结点
fv = find(e[i].v);
if (fu != fv) //保证非连通才能选边
{
std::cout << e[i].u << " " << e[i].v << " : " << e[i].w << std::endl;
tree[fu] = fv;
cnt++;
sum += e[i].w;
}
if (cnt == n - 1) break;//选到n-1条边就退出
}
std::cout << sum << std::endl;
}
int main()
{
std::cin >> n >> m;
int u, v;
for (int i = 1; i <= m; ++i)
{
std::cin >> u >> v >> w;
e[i].u = u;
e[i].v = v;
e[i].w = w;
}
sortt(1, m);
kruskal();
return 0;
}
kruskal算法的时间复杂度从中看出是取决于排序函数部分的,因为kruskal函数内的时间复杂度是O(m),而并查集经过路径压缩后查找时间复杂度是α(n)的,但是上图选择排序函数的时间复杂度是O(m*m)。也就是说根据选择的排序方法的不同,kruskal时间复杂度也就不同。
二、Prim算法:
Prim算法是以选点为中心,最终构成最小生成树的算法。
开始时,所有的点都不在生成树中;每次选择一个点加入生成树中;经过选n次点最终所有的点都在生成树中。
其中,prim中的几个概念要知道:1,点x到生成树的距离:点x到生成树中的点的直接边的距离;2,dis[]数组:存储的是顶点到生成树中的最小距离,dis[i]=k为顶点i到生成树的最小距离为k;3,flag[]数组:标记数组,标记顶点是否加入到生成树中。
选法:第一次选的点可以是任意点;第二次选的点为生成树中的点的邻接点中到生成树最小距离的邻接点,并且选好后要更新它的未被加入到生成树的邻接点到生成树的最小距离;重复这样操作选完n个点后最终构成的生成树就是最小生成树:因为每次选点完后所构成的部分都是最小的。
//Prim算法:以选点为中心,核心是每次选择一个点加入生成树中,最终选完n个点
//x到生成树的距离:即顶点x到生成树中的某些点的直接距离
//dis[]数组:存储的是顶点x到生成树的最小距离
//flag[]数组:标记数组,标记顶点是否加入到生成树中
//选法:第一次任意选择一个顶点加入。
//第二次选的点必须是生成树中的所有顶点中的邻接点,哪个邻接点距离生成树最近就选它,并且更新这个顶点的邻接点到生成树的最小距离
//以此类推,一直选完n个点,最终的生成树就是最小生成树。
#include <iostream>
#include <algorithm>
int e[105][105];//邻接矩阵
int n, m, w;
bool flag[105];//标记数组
int dis[105];//存储顶点到生成树的最小距离
const int inf = 10000;
void prim()
{
int s = 1;//任意选一个点,这里选择1顶点
dis[s] = 0;//设置它的最小距离为0,因为它被选了
int minn;//存每次选的点的编号
int tmp;//存每次选的边的边权
int sum = 0;//边权和
for (int i = 1; i <= n; ++i) //选n个点
{
minn = -1;//开始未选择点
tmp = inf;//开始没有距离
for (int j = 0; j < n; ++j)//遍历所有的点的dis值
{
if (!flag[j] && dis[j] < tmp)//只要点未被选并且dis值比tmp小就修改
{
tmp = dis[j];
minn = j;
}
}//遍历完后就是选择的点
sum += tmp;
std::cout << minn << " 点通过边权为 " << tmp << " 的边连接到生成树中" << std::endl;
flag[minn] = true;
//然后去更新它的未被访问过的邻接点的最小距离
for (int j = 0; j < n; ++j)
{
if (e[minn][j] != inf && !flag[j])
{
if (dis[j] > e[minn][j])
{
dis[j] = e[minn][j];
}
}
}
}
std::cout << sum << std::endl;
}
int main()
{
std::cin >> n >> m;
std::fill(&e[0][0], &e[0][0] + 105 * 105, inf);//开始认为都没边
std::fill(&dis[0], &dis[0] + 105, inf);//开始认为距离都为无穷大
int x, y;
for (int i = 1; i <= m; ++i)
{
std::cin >> x >> y >> w;
e[x][y] = e[y][x] = w;
}
prim();
return 0;
}
Prim算法的时间复杂度是O(n*n)的,因为在prim函数中对点的遍历是俩次循环(其实是一次选点次数循环加上一次遍历点的循环),它们的复杂度就是O(n*n),而邻接矩阵的遍历复杂度是O(n),即使是把邻接矩阵替换成邻接表(时间复杂度是O(n+m)),但整体的时间复杂度还是取决于对顶点的俩层循环。
下面为邻接表存法:
//邻接表存图
#include <iostream>
#include <algorithm>
typedef struct ENode {
int adj;//终点编号
int w;//边权
ENode* next;
}ENode;
struct Graph {
ENode* first;
}g[105];
int n, m, w;
bool flag[105];
int dis[105];
const int inf = 10000;
void prim()
{
int s = 1;//任意选一个点
dis[s] = 0;
int minn;//存储选择最小的顶点
int tmp;//存储最小的边权
int sum = 0;
for (int i = 1; i <= n; ++i) //选n次
{
minn = -1;
tmp = inf;
for (int j = 0; j < n; ++j) //遍历所有的点,选择到生成树最小距离的那个点
{
if (dis[j] < tmp && !flag[j])
{
tmp = dis[j];
minn = j;
}
}
sum += tmp;
std::cout << minn << " 点通过边权为 " << tmp << " 的边连接到生成树中" << std::endl;
flag[minn] = true;
//选完后去更新它的未被加入生成树中的邻接点的最小距离
ENode* p = g[minn].first;//找邻接点
while (p != NULL)
{
if (dis[p->adj] > p->w && !flag[p->adj])
{
dis[p->adj] = p->w;
}
p = p->next;
}
}
std::cout << sum << std::endl;
}
int main()
{
std::fill(&dis[0], &dis[0] + 105, inf);
std::cin >> n >> m;
for (int i = 0; i < n; ++i)//下标就是数据本身
g[i].first = NULL;
int x, y;
for (int i = 1; i <= m; ++i)
{
std::cin >> x >> y >> w;
ENode* e = new ENode;
e->w = w;
e->adj = y;
e->next = g[x].first;
g[x].first = e;
e = new ENode;
e->w = w;
e->adj = x;
e->next = g[y].first;
g[y].first = e;
}
prim();
return 0;
}
四、总结:
最小生成树的问题,简单得理解就是给定⼀个带有权值的连通图(连通网),从众多的生成树中
筛选出权值总和最小的生成树,即为该图的最小生成树。
最经典的两个最小生成树算法:Kruskal 算法与 Prim 算法。两者分别从不同的角度构造最小生成树,Kruskal 算法从边的⻆度出发,使用贪心的方式选择出图中的最小生成树,而 Prim 算法从顶点的角度出发,逐步找各个顶点上最小权值的边来构建最小生成树的。
最小生成树问题应用广泛,最直接的应用就是网线架设(网络G表示n各城市之间的通信线路网线 路(其中顶点表示城市,边表示两个城市之间的通信线路,边上的权值表示线路的长度或造价。可通过 求该网线的最小生成树达到求解通信线路或总代价最小的最佳方案。或者如果我们需要用最少的电线给一所房子安装电路。)、道路铺设。还可以间接应用于纠错的LDPC码、Renyi 熵图像配准、学习用于实时脸部验证的显著特征、减少蛋白质氨基酸序列测序中的数据存储,在湍流(turbulent)中模拟粒子交互的局部性,以及用于以太网桥接的自动配置,以避免在网络中形成环路。除此之外,最小生成树在聚类算法中也是应用广泛。