最小生成树算法
文章目录
一、前言
今天介绍最小生成树算法,最小生成树算法前提:(对应数据结构要掌握的)
Kruskal算法Prim算法
二、定义
生成树:仅针对于连通图,对于有n个顶点的连通图,至少有n-1条边,那么连接所有顶点的极小连通子图就是生成树
连通图:如果从任意一个顶点出发,沿着边总能走到任何其他顶点
非连通图不存在生成树,而是只存在生成森林
注:若在图的生成树上任意加上一条边,就必然形成回路
最小生成树 :对于连通网来说,边是带权值的,生成树的各边也带权值,因此把生成树的各边的权值总和称为生成树的权,把权值最小的生成树称为最小生成树
三、应用
-
铺设道路
-
建立通讯线路等
n个点,连边,形成的权值最小
四、Kruskal算法
4.1 算法思想
从小到大加入边 ---- 贪心算法
加边的同时,需通过并查集 判断该边的两个端点是否属于同一棵树,避免形成环。
4.2 步骤
- 把图上的每一条边存在一个边集数组里,数组的每个元素应有(起点,终点,边权)三个数据
- 将该边数组按边权从小到大排序
- 依次按边的边权从小到大枚举每一条边,如果边的两个端点已经连通了,那就跳过这条边(通过并查集判断)
- 否则把总答案累计上这条边
- 用并查集merge这条边的两个端点
- 返回第3步(循环这个过程)
4.3 时间复杂度
因为要遍历整个图,时间复杂度是O(n + e),再加上排序(O(eloge)),因此算法总体取决于排序的时间复杂度(冒泡: e 2 e^2 e2;选择: e 2 e^2 e2) e l o g e eloge eloge(快排)
4.4 代码
cpp
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=110; // 最大顶点数
const int maxm=10010; // 最大边数
struct Edge // 使用结构体存储每一条边,便于排序
{
int u, v, w; // 表示有一条(u,v)的无向边,边权为w
// 注意:这里的存边和链式前向星的存边不一样(单纯来存终点,起点,下一条边的编号)
} e[maxm];
int ecnt; // 用于边表计数
void addEdge(int u, int v, int w) // 加入一条无向边
{
++ecnt;
e[ecnt].u = u;
e[ecnt].v = v;
e[ecnt].w = w;
}
int fa[maxn]; // 并查集相关----father
int find(int x)
{
return x == fa[x] ? x : fa[x] = find(fa[x]); // 路径压缩
}
int n,m; // 顶点数 边数
bool cmp(const Edge &a, const Edge &b) // 根据边权w进行排序
{
return a.w < b.w;
}
int Kruskal() // Kruskal 算法核心过程
{
for(int i = 1; i <= n; i++)
{
fa[i] = i; // 初始化并查集
}
sort(e + 1, e + ecnt + 1, cmp); // 改写sort函数,只比较权值
int sum = 0; // 边权和
for(int i = 1; i <= ecnt; i++) // 枚举每条边
{
int u = e[i].u;
int v = e[i].v;
u = find(u);
v = find(v);
if(u != v)
{
fa[u] = v; // fa[v] = u;(都行,让一个点作另一个点的父亲)
sum += e[i].w;
}
}
return sum;
}
int main()
{
scanf("%d %d", &n, &m); // 有向图(只有1个add边)
int x, y, w;
for(int i = 1; i <= m; i++)
{
scanf("%d %d %d", &x, &y, &w);
addEdge(x, y, w); // 加到边集数组里
}
int ans = Kruskal();
printf("%d\n", ans);
return 0;
}
五、Prim算法
5.1 算法思想
同样基于贪心
5.2 定义
从一个节点开始,不断加点 (Kruskal是加边)。每次总是选出一个离生成树距离最小 的点去加入生成树,最后实现最小生成树。类似Dijkstra算法
5.3 步骤
- 将图的顶点分成2个集合,集合A = {NULL},用于存放最小生成树中的顶点。集合B = {all node},存放还未加入到生成树中的顶点。
- 在B中任选一顶点充当起始顶点,加入到集合A中,并更新A到B各顶点的距离dist[N]。
- 选择距离最近的一个顶点,加入到集合A中,并更新A到B各顶点的距离dist[N]...直到所有的顶点都加入到A中。
5.4 时间复杂度
O( v 2 v^2 v2)
放v个顶点,循环v次,找最短距离,又要循环v次
5.5 代码
cpp
#include<bits/stdc++.h>
using namespace std;
const int maxn = 5005;
const int inf = 0x7fffffff;
int cnt;
struct edge // 链式前向星的边集数组
{
int u, w, next;
} e[2 * maxn];
int head[maxn]; // 存储每个顶点的第一条边的编号
int dis[maxn]; // 维护每一点到最小生成树的距离
bool vis[maxn]; // 标记点是否被加入到最小生成树中
int n, m;
void add(int x, int y, int w) // 链式前向星的建边方法
{
cnt++; // 给这条边定一个编号
e[cnt].u = y; // 这条边从x出发指向y
e[cnt].w = w; // 边权为w
e[cnt].next = head[x]; // 把x之前的第一条边记成这条边的下一条边
head[x] = cnt; // 现在x的第一条边就是刚加的这条
}
int prim()
{
for(int i = 1; i <= n; i++)
{
dis[i] = inf; // dis数组是每一个点到最小生成树的距离
}
dis[1] = 0; // 出发点(看题目是否规定)
vis[1] = 1;
int now = 1;
for(int i = head[now]; i; i = e[i].next) // 链式前向星的遍历方法
{
int u = e[i].u; // 出边
dis[u] = min(dis[u], e[i].w);
}
int tot = 0;
int sum = 0; // 权值和
while(tot < n - 1) // 循环n - 1轮
{
int mindis = inf;
for(int i = 1; i <= n; i++)
{
if(!vis[i] && dis[i] < mindis)
{
now = i;
mindis = dis[i];
}
}
if(mindis == inf) // 可能是非连通图
{
return -1;
}
tot++;
sum += mindis;
vis[now] = 1;
for(int i = head[now]; i; i = e[i].next)// 链式前向星的遍历方法
{
int u = e[i].u;
if(vis[u])
{
continue;
}
dis[u] = min(dis[u], e[i].w);
}
}
return sum;
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= m; i++)
{
int x, y, z;
scanf("%d %d %d", &x, &y, &z); // 无向图
add(x, y, z);
add(y, x, z);
}
int ans = prim();
if(ans == -1)
{
printf("orz");
}
else
{
printf("%d", ans);
}
return 0;
}
加边方法:
- u, v, w或者x, y, w就是kruskal算法的加边方式
- u, w, next就是dijkstra算法的加边方式
六、解题
-
稠密图(边多)----
prim算法eloge就会超时,因此最好使用prim算法
-
边和点数目差不多----
Kruskal算法
七、题目
7.1 洛谷
- P1194 买礼物
- P1265 公路修建
八、小结
根据不同的题目,进行选择合适的算法进行求解