题目描述
如题,给出一个无向图,求出最小生成树,如果该图不连通,则输出orz。
输入格式
第一行包含两个整数 N,M,表示该图共有 N 个结点和 M 条无向边。
接下来 M 行每行包含三个整数 Xi,Yi,Zi,表示有一条长度为 Zi 的无向边连接结点 Xi,Yi。
输出格式
如果该图连通,则输出一个整数表示最小生成树的各边的长度之和。如果该图不连通则输出orz。
输入输出样例
输入 #1复制运行
4 5 1 2 2 1 3 2 1 4 3 2 3 4 3 4 3
输出 #1复制运行
7
说明/提示
数据规模:
对于 20% 的数据,N≤5,M≤20。
对于 40% 的数据,N≤50,M≤2500。
对于 70% 的数据,N≤500,M≤104。
对于 100% 的数据:1≤N≤5000,1≤M≤2×105,1≤Zi≤104,1≤Xi,Yi≤N。
样例解释:
所以最小生成树的总边权为 2+2+3=7。
1. 问题引入
在图论中,最小生成树是一个非常经典的问题:
在一个拥有N个顶点和M条边的带权无向图中,如何找到一棵树,使得它包含所有N个顶点,且所有边的权值之和最小?
此外,还有一个常见变种:如果图本身就是断开的(不连通),我们该如何判断?
本文将基于 Kruskal 算法,结合并查集,给出一种通解。
2. 核心算法:Kruskal (克鲁斯卡尔)
Kruskal 算法的核心思想是“贪心”。
算法步骤:
排序:将所有的边按照权值(w)从小到大排序。我们希望尽可能的选权值小的边。
遍历:按顺序枚举每一条边(u, v)。
判断:
如果u和v已经在同一个集合里(连通),说明加上这条边会形成环,跳过。
如果u和v不在同一个集合,说明这条边连接了两个独立的连通分量,选中它。
将u和v通过并查集合并。
终止:当我们选够了N-1条边时,最小生成树构建完成。
关键点:连通性判断
如果遍历完所有边,选中的边数仍然小于N-1,说明图是不连通的(有孤岛),此时无法生成 MST,按照题目要求输出 "orz" 。
3. 核心代码解析
3.1 结构体运算符重载
为了方便排序,我们直接在结构体内部重载<运算符。这样调用sort时就不需要写额外的cmp函数,代码封装性更好。
struct edge{ int x,y,w; //重载运算符 让边集数组按照边权从小到大排序 friend bool operator <(edge a,edge b){ return a.w<b.w; } }e[200010];//边集数组 edge mst[5010];//记录最小生成树每条边3.2 并查集的路径压缩
这是 Kruskal 能高效运行的保证。find函数在递归查找祖先的过程中,直接将沿途节点的父指针指向根节点,将树高度压扁。
int find(int x) { if(fa[x]==x) return x; return fa[x]=find(fa[x]); // 路径压缩,下次查就是 O(1) }4. 完整代码
//kruskal版 //首先明确什么是不连通,不连通代表图有多个连通分量 //即最后生成最小生成树后所用的边小于n-1(n是节点数) #include <iostream> #include <algorithm>//sort函数头文件 using namespace std; int fa[5010];//并查集 记录每个节点的父/祖先节点(集合中的老大) int n,m;//n个结点 m条无向边 int ans=0;//ans是mst(最小生成树)的边数 记录连了多少条边 //并查集查询+路径压缩 int find(int x){ //如果已经是根结点(集合老大)就返回 if(fa[x]==x) return x; //否则就递归找老大,并把老大给到沿途所有节点 return fa[x]=find(fa[x]); } void uni(int u,int v){ int fau=find(u);//找u老大 int fav=find(v);//找v老大 //如果老大相同就说明已经在一个集合就不需要操作 if(fau!=fav){//如果不同,就让u的老大变成v的老大的老大 fa[fav]=fau; } } struct edge{ int x,y,w; //重载运算符 让边集数组按照边权从小到大排序 friend bool operator <(edge a,edge b){ return a.w<b.w; } }e[200010];//边集数组 edge mst[5010];//记录最小生成树每条边 int main(){ ios::sync_with_stdio(false); cin.tie(0); cin>>n>>m;//n个结点 m条无向边 //初始化每个节点的老大为自己(每个点自己就是一个连通分量) for(int i=1;i<=n;i++) fa[i]=i; //创建边集数组 for(int i=1;i<=m;i++){ cin>>e[i].x>>e[i].y>>e[i].w; } //边集数组按照边权从小到大排序 sort(e+1,e+m+1); int ma=0;//记录最小生成树各边的长度之和 //取每一条边,看这条边的两端是不是同一个连通分量 for(int i=1;i<=m;i++){ int u=e[i].x; int v=e[i].y; //如果u v不连通 就把它两连起来 并把边权加进max //如果已经连通就跳过 if(find(u)!=find(v)){ ans++;//每连一条边 ans+1 //存最小生成树的边(这道题用不到) mst[ans].x=u; mst[ans].y=v; mst[ans].w=e[i].w; uni(u,v);//这条边的两端需要变成一个连通分量(集合) ma+=e[i].w; //最小生成树最多只能有n-1条边 if(ans==n-1) break; } } //如果 连接的边数==节点数-1,说明是连通图 if(ans==n-1) cout<<ma; //不等于 说明有多个连通分量 else cout<<"orz"; return 0; }5. 总结与易错点
关于 "orz":Kruskal算法不仅能求最短路径和,还能天然地判断图的连通性。如果循环结束后
ans<n-1,说明即使把所有能连的边都连上,图还是断开的。数据范围:
fa数组开N大小,e数组开M大小。不要搞混。