最小生成树
生成树 : 如果连通图G的一个子图是一棵包含G的所有顶点的树,则该子图称为G的生成树
最小生成树 : 边权和最小的生成树叫做最小生成树。如果原图不连通,则没有最小生成树
求最小生成树有两种方法 : prim 和 kurskal
一. prim算法
将最小生成树看做一个集合,每次选取距离集合最近的点,如果这个点不在集合中则加入集合,也就意味着这个点和集合里的点所连成的边加入了集合。最后等到所有点都加入集合则所有进入集合的边构成最小生成树。
例题1 prim板子
给你一张 n 个顶点 m 条边的无向简单连通图,顶点编号从 1 到 n,每条边都有一个边权,边权为非负整数。
请求出这张图的最小生成树,只需要输出最小生成树的边权和即可。
输入格式
第一行两个整数 n,m 表示图的顶点数、边数。接下来 m 行,每行三个整数 x,y,z 表示 x 号点与 y 号点之间有一条边权为 z 的边。
数据保证图中顶点两两连通。
输出格式
输出一行一个数,表示最小生成树的边权和。样例输入
4 4
1 2 1
2 3 3
3 4 1
1 4 2
样例输出
4
数据规模
对于所有数据,保证 2≤n≤1000,n−1≤m≤100000,1≤x,y≤n,1≤z≤10000
代码
# include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii;
const int N = 1e4+10;
vector<pii> edge[N];
int dist[N]; //这里的dist是指点到集合(生成树)的距离
bool st[N];
int n,m;
int prime()
{
memset(dist,0x3f,sizeof dist);
dist[1] = 0;
priority_queue<pii,vector<pii>,greater<pii> > q;
q.push({dist[1],1});
int ans = 0; //ans是最小生成树的权值和
int tot;tot = 0; //tot存的是集合中的点的数量,如果结束的时候tot != n则证明无法构造出生成树
while(q.size())
{
auto t = q.top();q.pop(); //找出距离集合最小的点
if(st[t.second]) continue; //每次取到集合最近的点,如果这个点已经在集合中则continue
tot++;
ans+=t.first;st[t.second] = true;
for(auto i:edge[t.second])
{
if(dist[i.first]>i.second) //注意这里dist要和边权比,因为边权最终是距离集合的距离
{
dist[i.first] = i.second;
q.push({dist[i.first],i.first});
}
}
}
if(tot!=n) return -1;
return ans;
}
int main()
{
cin>>n>>m;
for(int i=0;i<m;i++)
{
int x,y,z;scanf("%d%d%d",&x,&y,&z);
edge[x].push_back({y,z});edge[y].push_back({x,z});
}
cout<<prime()<<endl;
return 0;
}
例题2
在一片海域上有 n𝑛 个岛屿,第 ii 个岛屿的所在位置可以用二维坐标 (xi,yi) 表示,其中 xi𝑥𝑖, 都是整数。
我们想在这些岛屿之间修建桥梁,使得最终所有岛屿两两之间都可以通过桥梁到达。桥梁只能以岛屿作为起点和终点,桥梁的长度等于起点、终点两个岛屿的欧几里得距离。
由于经费有限,我们想花费最少的代价来修建桥梁(即建造的桥梁的长度和最小)。
请问建造的桥梁的长度和最小为多少?
输入格式
第一行一个整数 n。接下来 n 行,每行两个整数 xi,yi,代表一个岛屿的坐标。
输出格式
输出一行一个数表示建造的桥梁的最小长度和,答案保留一位小数输出。样例输入
3
0 0
4 4
0 4
样例输出
8.0
数据规模
对于所有数据,保证 2≤n≤800,0≤xi,yi≤1000两座岛屿不会出现在相同位置
我们可以先将每个点上连一条边,这样就可以建成一个n个点,n*(n-1)/2条边的无向图 然后再用prim求出这个图的最小生成树即可
代码
# include<bits/stdc++.h>
using namespace std;
typedef pair<int,double> pid;
typedef pair<double,int> pdi;
typedef pair<int,int> pii;
const int N = 810;
int n;
pii a[N];
vector<pid> edge[N];
bool st[N];
double dist[N];
double prim()
{
for(int i=1;i<=n;i++) dist[i] = 1e9;
dist[1] = 0;
priority_queue<pdi,vector<pdi>,greater<pdi> > q;
q.push({dist[1],1});
double ans = 0;
int tot = 0;
while(q.size())
{
auto t = q.top();q.pop();
if(st[t.second]) continue;
st[t.second] = true;tot++;ans+=t.first;
for(auto i:edge[t.second])
{
if(dist[i.first]>i.second )
{
dist[i.first] = i.second;
q.push({dist[i.first],i.first});
}
}
}
return ans;
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
int x,y;scanf("%d%d",&x,&y);
a[i] = {x,y};
}
for(int i=1;i<=n;i++)
{
for(int j=i+1;j<=n;j++)
{
int x1 = a[i].first,y1 = a[i].second;
int x2 = a[j].first,y2 = a[j].second;
double w = sqrt ( (x1-x2)*(x1-x2)+(y1-y2)*(y1-y2) );
edge[i].push_back({j,w});
edge[j].push_back({i,w});
}
}
printf("%.1f",prim());
return 0;
}
二. kurskal算法
相较于prim的一个个往集合里添加点得到最小生成树,它是以一条条添加边的方式来求最小生成树
大致流程 :
1.初始化并查集,一开始所有点的代表元都是它自己,每个点本身构成一个集合
2.将所有边按照边权从小到大的顺序排序
3.按边权从小到大的顺序依次枚举所有边,如果当前这条边连接的每个顶点位于同一个集合则continue,否则将这条边添加到最小生成树的边集里并将两点所在集合合并
4.直到只剩下一个集合,算法结束
例题1 Kurskal板子
给你一张 n 个顶点 m 条边的无向简单连通图,顶点编号从 1 到 𝑛,每条边都有一个边权,边权为非负整数。
请求出这张图的最小生成树,只需要输出最小生成树的边权和即可。
输入格式
第一行两个整数 n,m,表示图的顶点数、边数。接下来 m 行,每行三个整数 x,y,z 表示 x 号点与 y 号点之间有一条边权为 z 的边。
数据保证图中顶点两两连通。
输出格式
输出一行一个数,表示最小生成树的边权和。样例输入
4 4
1 2 1
2 3 3
3 4 1
1 4 2
样例输出
4
数据规模
对于所有数据,保证 2≤n≤50000,n−1≤m≤100000,1≤x,y≤n,1≤z≤10000
代码
# include<bits/stdc++.h>
using namespace std;
const int N = 5e4+10;
const int M = 1e5+10;
struct Edge
{
int u,v,w;
}edge[M]; //存每条边
int p[N]; //存每个元素的父节点
int n,m;
int find(int x) //找到x的祖先节点(并查集板子)
{
if(p[x]!=x) p[x] = find(p[x]);
return p[x];
}
bool cmp(Edge x,Edge y)
{
return x.w<y.w;
}
int kurskal()
{
int ans = 0,tot = 0; //ans是边权和,tot是加入集合的边的数量
for(int i=0;i<m;i++) //权值从小到大遍历每个边
{
int a = edge[i].u,b = edge[i].v;
if(find(a) == find(b)) continue; //如果一条边的两个端点在一个集合中,continue
p[find(a)] = find(b); //合并两个集合
ans+=edge[i].w;
tot++;
}
if(tot<n-1) return -1;
return ans;
}
int main()
{
cin>>n>>m;
for(int i=0;i<m;i++) cin>>edge[i].u>>edge[i].v>>edge[i].w;
sort(edge,edge+m,cmp);
for(int i=1;i<=n;i++) p[i] = i; //一开始把每个点的父节点都设为自己
cout<<kurskal()<<endl;
return 0;
}
例题2. 最小边权差生成树
给你一张 n 个顶点 m 条边的无向简单连通图,顶点编号从 1 到 n,每条边都有一个边权,边权为非负整数。
请求出这张图的所有生成树中,边权最大的边的边权与边权最小的边的边权的差值最小是多少。
输入格式
第一行两个整数 n,m,表示图的顶点数、边数。接下来 m 行,每行三个整数 x,y,z,表示 x 号点与 y 号点之间有一条边权为 z 的边。
数据保证图中顶点两两连通。
输出格式
输出一行一个数表示生成树的最小边权差。样例输入
4 4
1 2 1
2 3 4
3 4 5
1 4 6
样例输出
2
数据规模
对于所有数据,保证 2≤n≤5000,n−1≤m≤8000,1≤x,y≤n,1≤z≤10000
我们可以将所有边按边权从小到大排序,枚举每一条边是最小值的时候构成的生成树的最大边权差。一共做m次kurskal
代码
# include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii;
const int N = 8010;
struct Edge
{
int u,v,w;
}edge[N];
int n,m;
int p[N];
bool cmp(Edge x,Edge y)
{
return x.w<y.w;
}
int find(int x)
{
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main()
{
cin>>n>>m;
for(int i=0;i<m;i++) cin>>edge[i].u>>edge[i].v>>edge[i].w;
sort(edge,edge+m,cmp);
int ans = 0x3f3f3f3f;
for(int i=0;i<m;i++)
{
for(int j=1;j<=n;j++) p[j] = j;
int tot = 0;
for(int j=i;j<m;j++)
{
int a = edge[j].u,b = edge[j].v;
if(find(a) == find(b)) continue;
p[find(a)] = find(b);
tot++;
if(tot == n-1)
{
ans = min(ans,edge[j].w - edge[i].w);
break;
}
}
}
cout<<ans<<endl;
return 0;
}