Kruskal算法和Prim算法
这两个算法都是求最小生成树的算法,这里有个模板题
P3366 【模板】最小生成树
Kruskal算法与Prim算法相比比较好理解,基于贪心的思想,每次选取不会产生圈(重边)的权值最小的边,直到所有顶点连通。
Kruskal
具体做法:
- 将所有顶点看成一个独立的集合
- 将边按权值从小到大进行排序
- 利用并查集进行分组,如果一个边使得俩个顶点集合联通,那么就把这俩集合分成一组
- 直到所有顶点都属于一个集合
如果读者不会并查集,请先去学习并查集再来学最小生成树
这里使用的使优先队列,使用sort也是可以的,重写一下函数即可
- 创建一个小根堆,把所有边放入堆中,那么堆顶便是权值最小的边。
- 为题目结果,cnt为有多少边被使用,最小生成树的边为,其中为顶点个数
- 从堆顶获取最小的边,判断这个边所对应的俩顶点是否在同一组,不在同一组就把他俩合并到同一组里,然后并且更新的值
#define ufr(i, x, y) for (int i = x; i <= y; ++i) struct vertex { int from, to, cost; bool operator<(const vertex& v) const { return this->cost > v.cost; } }; priority_queue<vertex> que; ufr(i, 1, M) que.emplace(vs[i]); int ans = 0, cnt = 0; while (!que.empty()) { vertex ver = que.top(); que.pop(); if (!same(ver.from, ver.to)) { ans += ver.cost; ++cnt; unit(ver.from, ver.to); } }
最后一步判断是否可以生成一个最小生成树,就是判断与是否相等
Prim
这里存图使用了链式前向星
prim算法与Dijkstra算法比较相似,步骤:
- 创建一个数组,,表示最小生成树的起点,值代表最小生成树中以该顶点为终点的边的权值,初始值设置成无穷大。
- 更新以为起点的边,并将以这些边为终点的顶点,然后标记s已经访问过(这里跟Bellman Ford中遍历比较相似),更新其在数组中值。
- 然后循环遍历,在数组中找一个没访问过,并且在最小生成树中权值最小的顶点(说人话就是没访问过,并且在数组中值最小的索引值)
- 重复上面的步骤,直到所有顶点都访问过或者无顶点可以访问
解释一下各变量的意思:
- 就是本次访问的顶点,根据上面的说明,就是当前在最小生成树中,为访问的顶点,并且以该顶点()为终点的边比以其他未访问过的顶点为终点的边都小
- 答案,总共已经访问了几个顶点,顶点最多就只能访问N次,所以
while(tot++ < N)
。
- 数组代表访问过哪些点,数组代表在最小生成树中以索引为终点的边的最小权值
- 每次循环都要选择一个顶点,而就代表以那个顶点为终点的边的最小权值,如果一次遍历说明没找到,那么首先没到,说明处理的顶点少于,并且没顶点处理了,说明无法生成最小生成树
#define goto(x, y) x + 1, x + y + 1 #define mm(x, y) memset(x, y, sizeof(x)) #define ufr(i, x, y) for (int i = x; i <= y; ++i) struct { int to, w, next; } edge[MAX_N << 1]; int head[MAX_N], cnt = 0; int dis[MAX_N]; bool visited[MAX_N]; fill(goto(dis, N), inf); mm(visited, 0); int tot = 0, now; ll ans = 0; dis[1] = 0; while (tot++ < N) { int minn = inf; ufr(i, 1, N) if (!visited[i] && minn > dis[i]) { minn = dis[i]; now = i; } if (minn == inf) { f.pts("orz").ln(); return; } visited[now] = true; ans += minn; for (int i = head[now]; i; i = edge[i].next) { if (!visited[edge[i].to] && dis[edge[i].to] > edge[i].w) dis[edge[i].to] = edge[i].w; } }
小根堆优化Prim
普通Prim算法每次寻找权值最小的边需要重新迭代一遍数组,这里可以使用小根堆进行优化,因为只需要一个未访问的权值最小边即可,这里把数组中的值跟其索引(也就是顶点)组合为一个二元组放入小根堆中,中间的判断是,只处理未访问过的顶点和以该顶点为终点的边。
priority_queue<vertex> que; int tot = 0; ll ans = 0; que.emplace(1, 0); while (!que.empty() && tot < N) { vertex ver = que.top(); que.pop(); if (vis[ver.to]) continue; ans += ver.cost; vis[ver.to] = true; tot++; for (int i = head[ver.to]; i; i = edge[i].next) { if (!vis[edge[i].to]) que.emplace(edge[i].to, edge[i].w); } }
次小生成树
从点到点的次短路径无非就是两种情况
- 从的最短路径加上边
- 从(u,v)disdis2$,dis记录最短路,而dis2记录比dis大一点的路径。
解释代码:
if (dis2[v] < w) continue;
如果这个边比当前第二小的还大,那么就没必要进行判断了。- 第一个代表如果目标点比中的值小那么就交换他俩的值,为啥交换呢,因为中的值可能比小,所以要保留中的值
- 第二个如果处于和之间所以他是次小边,至少现在是的。
对于次小(长)生成树和次短(长)路径思路都是相同的,要记住一个要点。两个点的次短路径就是从到另一个点的最短路径加上边或者从点到另一个点的次短路径加上边。
mm(dis, 0x7f); mm(dis2, 0x7f); priority_queue<vertex> que; dis[1] = 0; que.emplace(1, 0); while (!que.empty()) { tie(v, w) = que.top().i2(); que.pop(); if (dis2[v] < w) continue; for (int i = head[v]; i; i = edge[i].next) { ll d = edge[i].w + w; if (dis[edge[i].to] > d) { swap(dis[edge[i].to], d); que.emplace(edge[i].to, dis[edge[i].to]); }; if (dis[edge[i].to] < d && d < dis2[edge[i].to]) { dis2[edge[i].to] = d; que.emplace(edge[i].to, dis2[edge[i].to]); } } } f.pt(dis2[N]).ln();
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库