最小生成树
一、最小生成树定义
最小生成树定义:在一张带权无向图中,最小生成树是一棵生成树,它的边权值之和最小。
什么是生成树?
生成树是一棵包含原图所有顶点的树,它的边的集合是原图的一个子集,并且任意两点之间有且只有一条简单路径。
二、常见求最小生成树的两种算法
1.Kruscal算法
具体步骤:
- 边按照权值排序
- 按照顺序依次加边
- 重复以上操作直到有最小生成树边集里面有
条边为止。
复杂度:
注意:
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N = 50100,M = 2e5+10; struct Node { int x,y,v; bool operator <(const Node &A)const{ return v<A.v; } }a[M+1]; int n,m,fa[N+1]; int find(int x) { if(x==fa[x])return x; return fa[x] = find(fa[x]); } int kruskal() { for(int i = 1;i<=n;i++)fa[i] = i; sort(a+1,a+1+m); int ans = 0,cnt = n; for(int i = 1;i<=m;i++) { int x = find(a[i].x),y = find(a[i].y); if(x!=y) { fa[x] = y; ans += a[i].v; --cnt; } if(cnt==1)break; } if(cnt!=1)return -1; return ans; } int main() { ios::sync_with_stdio(false),cin.tie(0),cout.tie(0); cin>>n>>m; for(int i = 1;i<=m;i++) { cin>>a[i].x>>a[i].y>>a[i].v; } int ans = kruskal(); if(ans==-1)cout<<"orz\n"; else cout<<ans<<endl; return 0; }
2.Prim算法
具体步骤:
- 随机选择一个顶点作为起点
- 将于起点相邻的边按照边权从小到大排序,并选择权值最小的边加入到最小生成树的边集里面。
- 将新加入的点加入到连通块中,并将于它相邻的边按照边权从小到大排序。
- 重复2,3步骤直到所有点是加入。
时间复杂度:
注意:
朴素写法:
完整写法,不一定是连通图情况
#include<bits/stdc++.h> using namespace std; struct Node{ int y,v; Node(int _y,int _v){y = _y;v = _v;}; }; std::vector<Node>edge[N+1]; int n,m,dist[N+1]; bool b[N+1]; int Prim() { memset(b,false,sizeof(b)); memst(dist,127,sizeof(dist)); dist[1] = 0; int ans = 0,tot = 0; while(1) { int x = -1; for(int i = 1;i<=n;i++) { if(!b[i]&&dist[i]<(1<<30)) { if(x==-1||dist[i]<dist[x]) x=i; } } if(x==-1)break;//找不到符合条件的能加入c中的了 ++tot; ans += dist[x]; b[x]=true; for(auto i:edge[x]) { dist[i.y] = min(dist[i.y],i.v);//dist记录的是当前这个点到c中的边权最小值 } } if(tot == n)return ans; else return -1;//不连通 } int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); return 0; }
堆优化写法
差别只在于最后循环体内的松弛操作。
最小生成树只关心所有边的和最小,所以有
最短路径树只搜索权值最小,所以有
#include<bits/stdc++.h> using namespace std; struct Node { int y,v; Node(int _y,int _v){y = _y;v = _v;}; }; const int N = 50000; int n,m,dist[N+1]; std::vector<Node> edge[N+1]; bool b[N+1]; set<pair<int,int>>q; void Prim() { memset(b,false,sizeof(b)); memset(dist,127,sizeof(dist)); dist[1] = 0; q.clear(); for(int i = 1;i<=n;i++) { q.insert({dist[i],i}); } int ans = 0; while(!q.empty()) { int x= q.begin()->second; q.erase(q.begin()); ans += dist[x]; b[x]=true; for(auto i:edge[x]) { if(!b[i.y]&&i.v<dist[i.y]) { q.erase({dist[i.y],i.y}); dist[i.y]=i.v; q.insert({dist[i.y],i.y}); } } } cout<<ans<<endl; } int main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin>>n>>m; for(int i= 1;i<=m;i++) { int x,y,v; cin>>x>>y>>v; edge[x].push_back({y,v}); edge[y].push_back({x,v}); } Prim(); return 0; }
三、最小生成树常见应用
(1)对于Kruscal原理的理解:
例题1:P1396 营救
题意:给你起点
思路:因为我们知道用
#include<bits/stdc++.h> using namespace std; const int N = 10000,M = 20000; struct Node { int x,y,v; bool operator <(const Node &A)const{ return v<A.v; } }a[M+1];//存边 int n,m,s,t,fa[N+1]; int findset(int i) { if(i==fa[i])return i; return fa[i]=findset(fa[i]); } void Kruskal() { for(int i =1 ;i<=n;i++) { fa[i] =i; } sort(a+1,a+1+m); for(int i = 1;i<=m;i++){ int x= findset(a[i].x),y = findset(a[i].y); if(x!=y) { fa[x] = y; } if(findset(s)==findset(t)){ cout<<a[i].v<<endl; return; } } } int main() { cin>>n>>m>>s>>t; for(int i = 1;i<=m;i++) { cin>>a[i].x>>a[i].y>>a[i].v; } Kruskal(); return 0; }
当然了,这道题的解法还有很多。求最大值最小,二分+
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N = 1e5+10,M = 2e5+10; const int LOGN = 20; struct Node { int x,y,v; }a[M+1]; int n,m,s,t,now,fa[N][LOGN+2],val[N],dep[N]; vector<int>e[N]; //////////////////////////////DSU////////////////////////////////////// struct Dsu { int fa[N]; void init(int x) {for(int i = 1;i<=x;i++) fa[i] = i;} int find(int x) {return x == fa[x] ? x : fa[x] = find(fa[x]);} void merge(int x, int y) { int u = find(x), v = find(y); if(u == v) return; fa[u] = v; } }dsu; /////////////////////////Kruscal/////////////////////////////////////// void kruskalRebuildTree() { dsu.init(2*n-1); sort(a+1, a+1+m, [](const Node& x, const Node& y) { return x.v < y.v; }); now = n; for(int i = 1;i<=m;i++) { ll u = dsu.find(a[i].x), v = dsu.find(a[i].y), w = a[i].v; if(u != v) { val[++now] = w; dsu.merge(u, now); dsu.merge(v, now); e[now].push_back(u); e[now].push_back(v); } } } /////////////////////////LCA///////////////////////////////////////// void dfs(int u, int from) { dep[u] += dep[from] + 1; for(auto v : e[u]) { if(v == from) continue; fa[v][0] = u; dfs(v, u); } } void lca_init(int now) { for(int j = 1; j < LOGN; j++) for(int i = 1; i <= now; i++) fa[i][j] = fa[fa[i][j - 1]][j - 1]; } int lca_query(int u, int v) { if(dep[u] < dep[v]) swap(u, v); int d = dep[u] - dep[v]; for(int j = LOGN; j >= 0; j--) if(d & (1 << j)) u = fa[u][j]; if(u == v) return u; for(int j = LOGN; j >= 0; j--) { if(fa[u][j] != fa[v][j]) u = fa[u][j],v = fa[v][j]; } return fa[u][0]; } /////////////////////////////Main////////////////////////////////////////////// int main() { cin>>n>>m>>s>>t; for(int i = 1;i<=m;i++) { cin>>a[i].x>>a[i].y>>a[i].v; } kruskalRebuildTree(); //可能是森林 for(int i = now;i>=1;i--) { if(!dep[i]) dfs(i,0); } lca_init(now); int lca = lca_query(s,t); if(!lca)cout<<-1<<"\n"; else cout<<val[lca]<<"\n"; return 0; }
例题2:P2700 逐个击破
题意:花费最小代价,使得给出的
思路:可以反向思考,我们要花费最小代价使得他们不连通,那是不是可以反着思考?
把删除边权小的转化为加入边权大的。用
❗记得,如果把这条边加进来了,如果有一个是
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N = 1e5+10,M = 2e6+10; struct Node { int x,y,v; bool operator <(const Node &A)const{ return v>A.v; } }a[M+1]; int n,m,k,fa[N+1]; set<int>c; int cnt = 0; int find(int x) { if(x==fa[x])return x; return fa[x] = find(fa[x]); } ll kruskal() { for(int i = 1;i<=n;i++)fa[i] = i; sort(a+1,a+1+m); ll ans = 0; for(int i = 1;i<=m;i++) { int x = find(a[i].x),y = find(a[i].y); if(c.find(x)!=c.end()&&c.find(y)!=c.end())continue; if(x!=y) { fa[x] = y; ans += a[i].v; } if(c.find(x)!=c.end()) c.insert(y); else if(c.find(y)!=c.end()) c.insert(x); } return ans; } int main() { ios::sync_with_stdio(false),cin.tie(0),cout.tie(0); cin>>n>>k; m = n-1; ll res = 0; for(int i = 1;i<=k;i++) { int x; cin>>x; x++; c.insert(x); } for(int i = 1;i<=m;i++) { int x,y,v; cin>>x>>y>>v; x++,y++; res += v; a[i].x = x,a[i].y = y,a[i].v = v; } ll ans = kruskal(); cout<<res-ans<<endl; //cout<<"ans = "<<ans<<endl; return 0; }
(2)结合超级源点:
例题1:P1194 买礼物
题意:要买
思路:
- 将每件物品看作一个结点,物品之间有费用或者优惠为权值的边。
- 设置一个超级源点
号结点,物品与该节点相连表示直接花费 元去买。 - 从
向每个结点连权值为 的边,其他的按照优惠正常连边 - 跑
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N = 5010,M = 2e6+10; struct Node { int x,y,v; bool operator <(const Node &A)const{ return v<A.v; } }a[M+1]; int n,m,A,fa[N+1]; int cnt; int find(int x) { if(x==fa[x])return x; return fa[x] = find(fa[x]); } ll kruskal() { for(int i = 1;i<=n;i++)fa[i] = i; sort(a+1,a+1+cnt); ll ans = 0; for(int i = 1;i<=cnt;i++) { int x = find(a[i].x),y = find(a[i].y); if(x!=y) { fa[x] = y; ans += a[i].v; } } return ans; } int main() { ios::sync_with_stdio(false),cin.tie(0),cout.tie(0); cin>>A>>n; for(int i = 1;i<=n;i++) { a[++cnt].x = 0; a[cnt].y = i; a[cnt].v = A; } for(int i = 1;i<=n;i++) { for(int j = 1;j<=n;j++) { int v; cin>>v; if(v==0||i>=j)continue; a[++cnt].x = i; a[cnt].y = j; a[cnt].v = v; } } ll ans = kruskal(); cout<<ans<<endl; return 0; }
例题2:P1550Watering Hole G
思路:和上题思路一样,同样是建立一个超级源点,与它相连的话表示挖一口井,其他供水正常连边,跑
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N = 1e5+10,M = 2e6+10; int n,cnt,fa[N+1],w[N]; struct Node { int x,y,v; bool operator <(const Node &A)const{ return v<A.v; } }a[M+1]; int find(int x) { if(x==fa[x])return x; return fa[x] = find(fa[x]); } int kruskal() { for(int i = 1;i<=n;i++)fa[i] = i; sort(a+1,a+1+cnt); int ans = 0; for(int i = 1;i<=cnt;i++) { int x = find(a[i].x),y = find(a[i].y); if(x!=y) { fa[x] = y; ans += a[i].v; } } return ans; } int main() { ios::sync_with_stdio(false),cin.tie(0),cout.tie(0); cin>>n; for(int i = 1;i<=n;i++) { cin>>w[i]; a[++cnt].x = 0,a[cnt].y = i,a[cnt].v = w[i]; } for(int i = 1;i<=n;i++) { for(int j = 1;j<=n;j++) { int x; cin>>x; if(i<=j)continue; a[++cnt].x = i,a[cnt].y = j,a[cnt].v = x; } } ll ans = kruskal(); cout<<ans<<endl; return 0; }
例题3:Shichikuji and Power Grid
思路和上面类似,只不过两个之间的花费需要自己计算出来,这里就不赘述啦QAQ。
#include<bits/stdc++.h> using namespace std; #define int long long typedef long long ll; const int N = 2e3+10; struct Node { int x,y; ll v; bool operator <(const Node &A)const{ return v<A.v; } }a[N*N]; struct ty { int x,y; }p[N]; int n,m,fa[N+1],cnt,k[N]; int find(int x) { if(x==fa[x])return x; return fa[x] = find(fa[x]); } set<int>s; set<pair<int,int>>l; ll kruskal() { for(int i = 1;i<=n;i++)fa[i] = i; sort(a+1,a+1+cnt); ll ans = 0; for(int i = 1;i<=cnt;i++) { int x = find(a[i].x),y = find(a[i].y); if(x!=y) { fa[x] = y; ans += a[i].v; if(a[i].x==0) s.insert(a[i].y); else l.insert({a[i].x,a[i].y}); } } return ans; } ll get(ty a,ty b) { return 1ll*abs(a.x-b.x)+abs(a.y-b.y); } signed main() { ios::sync_with_stdio(false),cin.tie(0),cout.tie(0); cin>>n; for(int i = 1;i<=n;i++) { cin>>p[i].x>>p[i].y; } for(int i = 1;i<=n;i++) { int x; cin>>x; a[++cnt].x = 0,a[cnt].y = i,a[cnt].v = x; } for(int i = 1;i<=n;i++) cin>>k[i]; for(int i = 1;i<=n;i++) { for(int j = i+1;j<=n;j++) { ll cost = k[i]+k[j]; ll dis = get(p[i],p[j]); cost *= dis; a[++cnt].x = i,a[cnt].y = j,a[cnt].v = cost; } } cout<<kruskal()<<"\n"; cout<<s.size()<<"\n"; for(auto& x:s) cout<<x<<" "; if(s.size()) cout<<"\n"; cout<<l.size()<<"\n"; for(auto& [x,y]:l) cout<<x<<" "<<y<<"\n"; return 0; }
(3)Kruskal重构树
分析:
我们来讲解
构造方法如下:
- 按照边权大小排序(按你的需求,如果是找点
路径上的边最小值权值的最大值需要按照从大到小排序,因为这样建出来的是小根堆。反之从小到大排序的话,是一个大根堆,可以找到点 的简单路径上的的边的最大权值的最小值) - 如果
以边权 的边相连,求出并查集中 的代表元 。如果他们不在一个集合里面,我们就新建一个结点,设该节点为 ,点权是 【这步实现了边权到点权的转化】,然后把 在并查集中的代表元都设为 。那么,询问 的答案的时候,二者的 的点权就是答案。
以下面这题为例,
例题1:P1967 货车运输
按照样例建出的树:
//样例 1 2 4 2 3 3 3 1 1
关于正确性?
如果
按照我们的求法能得到类似答案——它们一开始不相连,在这一步后代表元素(两个根结点)连接权值为
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N = 1e5+10,M = 2e5+10; const int LOGN = 20; struct Node { int x,y,v; }a[M+1]; int n,m,q,now,fa[N][LOGN+2],val[N],dep[N]; vector<int>e[N]; //////////////////////////////DSU////////////////////////////////////// struct Dsu { int fa[N]; void init(int x) {for(int i = 1;i<=x;i++) fa[i] = i;} int find(int x) {return x == fa[x] ? x : fa[x] = find(fa[x]);} void merge(int x, int y) { int u = find(x), v = find(y); if(u == v) return; fa[u] = v; } }dsu; /////////////////////////Kruscal//////////////////////////////////// void kruskalRebuildTree() { dsu.init(2*n-1); sort(a+1, a+1+m, [](const Node& x, const Node& y) { return x.v > y.v;//重构最大生成树 }); now = n; for(int i = 1;i<=m;i++) { ll u = dsu.find(a[i].x), v = dsu.find(a[i].y), w = a[i].v; if(u != v) { val[++now] = w; dsu.merge(u, now); dsu.merge(v, now); e[now].push_back(u); e[now].push_back(v); } } } /////////////////////////LCA/////////////////////////////// void dfs(int u, int from) { dep[u] += dep[from] + 1; for(auto v : e[u]) { if(v == from) continue; fa[v][0] = u; dfs(v, u); } } void lca_init(int now) { for(int j = 1; j < LOGN; j++) for(int i = 1; i <= now; i++) fa[i][j] = fa[fa[i][j - 1]][j - 1]; } int lca_query(int u, int v) { if(dep[u] < dep[v]) swap(u, v); int d = dep[u] - dep[v]; for(int j = LOGN; j >= 0; j--) if(d & (1 << j)) u = fa[u][j]; if(u == v) return u; for(int j = LOGN; j >= 0; j--) { if(fa[u][j] != fa[v][j]) u = fa[u][j],v = fa[v][j]; } return fa[u][0]; } /////////////////////////////////////////////////////////////// int main() { cin>>n>>m; for(int i = 1;i<=m;i++) { cin>>a[i].x>>a[i].y>>a[i].v; } kruskalRebuildTree(); //可能是森林 for(int i = now;i>=1;i--) { if(!dep[i]) dfs(i,0); } lca_init(now); cin>>q; while(q--) { int s,t; cin>>s>>t; int lca = lca_query(s,t); if(!lca)cout<<-1<<"\n"; else cout<<val[lca]<<"\n"; } return 0; }
例题2:Labyrinth
题意:
给定一张
你需要从点
注意初始身体宽度必须是正整数。若无解输出
思路:瓶颈是每个连通块里面限制最小的边,我们进行
我们不知道先走左树还是右树,所以都需要考虑。
如果先走左边,那么答案是
那么以上表达式需要同时满足,我们的答案
最终就是:
#include<bits/stdc++.h> #define INF 0x3f3f3f3f #define int long long using namespace std; typedef long long ll; const int N = 4e5+10,M = 5e5+10; const int LOGN = 20; struct Node { int x,y,v; }a[M+1]; ll n,m,q,now,fa[N][LOGN+2],val[N],dep[N]; vector<int>e[N]; ll ls[N],rs[N],sum[N],c[N],dp[N]; //////////////////////////////DSU////////////////////////////////////// struct Dsu { int fa[N]; void init(int x) {for(int i = 1;i<=x;i++) fa[i] = i;} int find(int x) {return x == fa[x] ? x : fa[x] = find(fa[x]);} void merge(int x, int y) { int u = find(x), v = find(y); if(u == v) return; fa[u] = v; } }dsu; /////////////////////////Kruskal//////////////////////////////////// void kruskalRebuildTree() { dsu.init(2*n-1); sort(a+1, a+1+m, [](const Node& x, const Node& y) { return x.v > y.v;//重构最大生成树 }); now = n; for(int i = 1;i<=m;i++) { ll u = dsu.find(a[i].x), v = dsu.find(a[i].y), w = a[i].v; if(u != v) { val[++now] = w; dsu.merge(u, now); dsu.merge(v, now); // e[now].push_back(u); // e[now].push_back(v); ls[now] = u; rs[now] = v; } } } ////////////////////////////DP/////////////////////////////////// void dfs(int u) { if(!ls[u]||!rs[u]) { sum[u] = c[u]; dp[u] = INF; return; } dfs(ls[u]),dfs(rs[u]); sum[u] = sum[ls[u]] + sum[rs[u]]; // 先吃ls[u] dp[u] = max(min(dp[rs[u]],val[u])-sum[ls[u]],min(dp[ls[u]],val[u])-sum[rs[u]]); } ////////////////////////////Main/////////////////////////////////// signed main() { cin>>n>>m; for(int i = 1;i<=n;i++)cin>>c[i]; for(int i = 1;i<=m;i++) { cin>>a[i].x>>a[i].y>>a[i].v; } kruskalRebuildTree(); dfs(now); if(dp[now]<=0) cout<<-1<<"\n"; else cout<<dp[now]<<"\n"; return 0; }
例题3:[ARC098F] Donation
这是一道和上一题很像的题目。
题意:
给出一个
最开始时你拥有一定数量的钱,并且可以选择这张图上的任意一个点作为起始点,之后你从这个点开始沿着给定的边遍历这张图。每当你到达一个点
你需要对所有的
思路:我们令
我们按照
和上题思路类似,差不多就是反过来。
思路:瓶颈是每个连通块里面限制最大的边,我们进行
我们不知道先走左树还是右树,所以都需要考虑。
如果先走左边,那么答案是
那么以上表达式需要同时满足,我们的答案
最终就是:
还要注意一下叶子节点的处理,对于一个叶子节点,它的
以上就大功告成啦!你将获得一份和上一次几乎一样滴代码
#include<bits/stdc++.h> #define INF 0x3f3f3f3f #define int long long using namespace std; typedef long long ll; const int N = 4e5+10,M = 5e5+10; const int LOGN = 20; struct Node { int x,y,v; }a[M+1]; ll n,m,q,now,fa[N][LOGN+2],val[N],dep[N]; vector<int>e[N]; ll ls[N],rs[N],sum[N],c[N],dp[N],A[N],B[N]; //////////////////////////////DSU////////////////////////////////////// struct Dsu { int fa[N]; void init(int x) {for(int i = 1;i<=x;i++) fa[i] = i;} int find(int x) {return x == fa[x] ? x : fa[x] = find(fa[x]);} void merge(int x, int y) { int u = find(x), v = find(y); if(u == v) return; fa[u] = v; } }dsu; /////////////////////////Kruskal//////////////////////////////////// void kruskalRebuildTree() { dsu.init(2*n-1); sort(a+1, a+1+m, [](const Node& x, const Node& y) { return x.v < y.v; }); now = n; for(int i = 1;i<=m;i++) { ll u = dsu.find(a[i].x), v = dsu.find(a[i].y), w = a[i].v; if(u != v) { val[++now] = w; dsu.merge(u, now); dsu.merge(v, now); // e[now].push_back(u); // e[now].push_back(v); ls[now] = u; rs[now] = v; } } } ////////////////////////////DP/////////////////////////////////// void dfs(int u) { if(!ls[u]||!rs[u]) { sum[u] = B[u]; dp[u] = c[u]+B[u]; return; } dfs(ls[u]),dfs(rs[u]); sum[u] = sum[ls[u]] + sum[rs[u]]; dp[u] = min(max(dp[rs[u]],val[u])+sum[ls[u]],max(dp[ls[u]],val[u])+sum[rs[u]]); } //////////////////////////////Main///////////////////////////////// signed main() { cin>>n>>m; for(int i = 1;i<=n;i++){ cin>>A[i]>>B[i]; c[i] = max(A[i]-B[i],0ll); } for(int i = 1;i<=m;i++) { cin>>a[i].x>>a[i].y; a[i].v = max(c[a[i].x],c[a[i].y]); } kruskalRebuildTree(); dfs(now); cout<<dp[now]<<"\n"; return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!