最小生成树Kruskal+Prim算法
最小生成树
给定一张边带权的无向图G=(V,E),n=|V|,m=|E|。由 V中全部n个顶点和E中n-1条边构成的无向连通子图被称为G的一棵生成树。边的权值之和最小的生成树被称为无向图G的最小生成树。
定理:任意一棵最小生成树一定包含无向图中权值最小的边。
证明:反证法。假设无向图中存在一棵最小生成树不包含权值最小的边。设e=(u,v,w)是权值最小的边。把e加到树上,e会和树上从x到y的路经一起构成一个环,并且环上其他边的权值都比e大。因此,用e代替环上的其他任意一条边,会形成一棵权值更小的生成树,与假设矛盾。所以假设不成立,原命题成立。
Kruskal算法
1.算法思想:Kruskal算法就是基于上述定理。最初,可认为每个节点各自构成一棵仅包含一个点的树(各自为一个集合)。每次从未被选取的边中选取一条权值最小且该边的两个端点不在一棵树上(不在一个集合里),把该边加入到最小生成树,该边的两个端点合成一棵树(放入一个集合),直至所有的点都在一棵树上(都在一个集合里)。显而易见,判断两个端点是否在一个集合里与合并可以用并查集简单地完成。
2.算法流程:
(1)初始化,建立并查集,每个点各自构成一个集合;
(2)把所有边按照权值从小到大排序,依次扫描每条边(u,v,w);
(3)若u,v属于同一个集合,则继续扫描下一条边,否则合并u,v所在集合,并把w累加到答案ans里;
(4)当最小生成树的集合中的节点个数为n,或者合并的边数为n-1时,ans即为所求的最小生成树的边的权值之和。
3.时间复杂度为O(m log m)。
4.例题:洛谷P3366 https://www.luogu.com.cn/problem/P3366
AC代码如下:
1 #include<bits/stdc++.h>
2 #define pa pair<int,int>
3 using namespace std;
4 const int N=1e6+5;
5 int n,m,fa[N],cnt,ans;
6 pair<int,pa> e[N];
7 int f(int x)
8 {
9 if(x==fa[x]) return x;
10 return fa[x]=f(fa[x]);//路径压缩
11 }//并查集
12 void kruskal()
13 {
14 for(int i=1;i<=n;i++) fa[i]=i;//初始化,每个点各自为一个集合
15 sort(e+1,e+m+1);//排序
16 for(int i=1;i<=m;i++)
17 {
18 int u=e[i].second.first,v=e[i].second.second;
19 u=f(u);
20 v=f(v);
21 if(u!=v)//两个点是否在一个集合里
22 {
23 fa[u]=v;//合并
24 ans+=e[i].first;//累加答案
25 cnt++;//合并边数加1
26 }
27 if(cnt==n-1) break;//满足条件跳出循环
28 }
29 if(cnt==n-1) cout<<ans;//若扫描完所有边,合并边数仍小于n-1,则该图不连通
30 else cout<<"orz";
31 }
32 int main()
33 {
34 cin>>n>>m;
35 for(int i=1;i<=m;i++)
36 {
37 int x,y,w;
38 cin>>x>>y>>w;
39 e[i]=make_pair(w,make_pair(x,y));
40 }
41 kruskal();
42 return 0;
43 }
Prim算法
1.算法思想:Prim算法同样基于上述定理。最初,Prim算法仅确定1号节点属于最小生成树。设已经确定属于最小生成树的节点集合为T,剩余节点集合为S,每次找到两个端点分别属于集合S,T的权值最小的边(u,v,w),然后把点u从集合S中删除,加入到集合T中,并把w累加到答案中。
具体来说,可以维护一个数组d:若u属于S,则d[u]表示节点u与集合T中的节点之间权值最小的边的权值。若u属于T,则d[u]就等于u被加入T是选出的最小边的权值。而我们可以用一个标记数组vis,标记节点是否属于T,每次我们从未标记的节点中选出d值最小的,把它标记(新加入T)。同时扫描所有出边,更新另一个端点的d值。最后,最小生成树的权值之和就是d[1]+d[2]+......+d[n]。
2.算法流程:
(1)初始化,d[1]=0,其他节点d值全为正无穷;
(2)从未标记的节点中选出d值最小的u,把它标记vis[u]=1,并把d[u]累加到答案中;
(3)扫描(2)中选出节点所有出边,更新另一个端点的d值;
(4)重复(2)(3),直至所有点全被标记。
3.注意事项:时间复杂度为O(n2),可以用二叉堆(优先队列priority_queue)优化到O(m log n)。但用二叉堆优化不如直接用Kruskal算法更加方便。因此,Prim算法主要用于稠密图,尤其是完全图的最小生成树的求解。
4.例题:洛谷P3366 https://www.luogu.com.cn/problem/P3366
AC代码如下:
1 #include<bits/stdc++.h>
2 #define pa pair<int,int>
3 using namespace std;
4 const int N=1e6+5;
5 int n,m,vis[N],d[N],cnt,ans;
6 vector<pa>g[N];
7 void prim()
8 {
9 memset(d,0x3f,sizeof(d));
10 memset(vis,0,sizeof(vis));
11 d[1]=0;//初始化
12 while(1)
13 {
14 int u=-1;
15 for(int i=1;i<=n;i++)
16 if(vis[i]==0&&(u==-1||d[u]>d[i])) u=i;//从未标记的节点中选出d值最小的u
17 if(u==-1||d[u]==0x3f3f3f3f) break;//所有节点被标记或该图不连通跳出循环
18 ans+=d[u];//累加答案
19 cnt++;//标记节点数+1
20 vis[u]=1;//标记
21 for(int i=0;i<g[u].size();i++)
22 {
23 int v=g[u][i].first,w=g[u][i].second;
24 if(vis[v]) continue;
25 d[v]=min(d[v],w);//更新所有出边另一个端点的d值
26 }
27 }
28 if(cnt==n) cout<<ans;//若被标记节点数不等于n,则该图不连通
29 else cout<<"orz";
30 }
31 int main()
32 {
33 cin>>n>>m;
34 for(int i=1;i<=m;i++)
35 {
36 int x,y,w;
37 cin>>x>>y>>w;
38 g[x].push_back(make_pair(y,w));
39 g[y].push_back(make_pair(x,w));
40 }
41 prim();
42 return 0;
43 }
练习:洛谷P2872 https://www.luogu.com.cn/problem/P2872
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理