目录
一、知识点
1. 生成树定义:在一个有n个点的无向连通图中,取其n-1条边并连接所有的顶点,所得到的子图称为原图的一棵生成树。
2. 树的属性:无环+连通+任意两点之间只有唯一的简单路径+删掉任意边就不连通
3. 最小生成树:各边权和最小的一棵生成树。
4. 最小边原则:图中权值最小的边(如果唯一的话)一定在最小生成树上
5. 唯一性定理:对于一个图,如果各边权值不等,则图的MST一定是唯一的,反之不成立
计算无向图的最小生成树
1. Prime
算法思路:贪心
(1)最初将无向连通图分成两个顶点集合A、B,任选一个顶点a先放到A,将B中与a有关并且权值最小的点加到A,知道n个顶点全部属于A结束。
- 注意这里的d数组,存的是到树的最短路径,不是到源点的最短路径。这里注意区别单源最短路径。所以,每次更新d数组时,比较的是d[i]与g[k][i]而不用加上ans。
(2)显然出发点不同,最小生成树的形态就不同,但边权和的最小值是唯一的。
复杂度:O(N^2)
//prime算法
#include<bits/stdc++.h>
using namespace std;
const int inf=0x3f3f3f3f;
const int maxn=505;
int vis[maxn];//标记顶点i是否加入最小生成树中
int d[maxn];//表示点i与当前生成树中的点有连边的边长的最小值
int g[maxn][maxn];//存边权
int n,m,ans;
void read()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)g[i][j]=inf;
for(int i=1;i<=m;i++)
{
int x,y,w;
scanf("%d%d%d",&x,&y,&w);
g[x][y]=g[y][x]=w;
}
}
void Prim(int v0)
{
memset(vis,0,sizeof(vis));//初始化生成树点集
for(int i=1;i<=n;i++)d[i]=inf;
d[v0]=0;ans=0;
int minn,k;
for(int i=1;i<=n;i++)//选择n个点
{
minn=inf;
for(int j=1;j<=n;j++)
if(!vis[j] && minn>d[j])
{
minn=d[j];
k=j;
}
vis[k]=1;//标记;
ans+=d[k];//算最小生成树的边权和
for(int j=1;j<=n;j++)//修改d数组
if(!vis[j] && d[j]>g[k][j])//这里注意区别单源最短路径
d[j]=g[k][j];
}
}
int main()
{
read();
Prim(1);
cout<<ans<<endl;
return 0;
}
2. Kruskal
算法思路:贪心
(1)将图中的所有边都去掉。
(2)将边按权值由小到大添加到图中,并保证添加的过程中不会形成环
(3)重复上一步直到连接所有顶点
该方法用到了并查集来判断是否会产生环
复杂度:O(mlogm+mα(n) ) //α(n)是一次并查集的复杂度
//kruskal算法
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
struct edge{
int x,y,z;
}a[maxn];
bool cmp(edge x,edge y)
{
return x.z<y.z;
}
int n,m,pre[maxn],ans,flag;
int findd(int x)
{
if(pre[x]==x)return x;
pre[x]=findd(pre[x]);
return pre[x];
}
void kruskal()
{
for(int i=1;i<=n;i++)pre[i]=i;
int k=0;
for(int i=1;i<=m;i++)
{
int f1=findd(a[i].x);
int f2=findd(a[i].y);
if(f1!=f2)
{
ans+=a[i].z;
pre[f1]=f2;
k++;
if(k==n-1)break;//最小生成树的边数为n-1
}
}
if(k<n-1)
{
puts("impossiable");
flag=0;
return;
}
}
int main()
{
scanf("%d%d",&n,&m);
ans=0;flag=1;
for(int i=1;i<=m;i++)
scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].z);
sort(a+1,a+1+m,cmp);
kruskal();
if(flag)printf("%d\n",ans);
return 0;
}
二、例题
1. 【loj】#10064. 「一本通 3.1 例 1」黑暗城堡(最短路径生成树 dijkstra+Prim)
2. 【loj】#10066. 「一本通 3.1 练习 1」新的开始 (最小生成树·Prim)
3. 【loj】#10067. 「一本通 3.1 练习 2」构造完全图(最小生成树 Kruskal)
题目描述:
对于完全图 G,若有且仅有一棵最小生成树为 T,则称完全图 G 是树 T 扩展出的。
给你一棵树 T,找出 T 能扩展出的边权和最小的完全图 G。
【分析】给出最小生成树,并且说明了该MST形态唯一。类比Kruskal算法。并查集。
先把边按权值由小到大排序,然后遍历边。注意,边数是n-1;
把图的顶点集分为两个集合。集合中的点数size[x], size[y],因为是完全图,所以任意两点之间都是有边直接相连的。
所以连通两个集合使其变成完全图一共需要cnt=size[x]*size[y]条边
而遍历边的时候,已经存在一条,所以只需要再加cnt-1条边即可。而边权比新加入的这条边的边权+1。
所以核心式子就是(size[x]*size[y]-1)*(a[i].d+1)
注意计算的时候,要强制转换为long long....不然!就一直wa....╥﹏╥
【代码】
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
typedef long long ll;
int pre[maxn],size[maxn];
ll ans,n;
struct node{
int l,r,d;
}a[maxn];
bool cmp(node x,node y)
{
return x.d<y.d;
}
int findd(int x)
{
return x==pre[x]?x:pre[x]=findd(pre[x]);
}
int main()
{
ans=0;
scanf("%lld",&n);
for(int i=1;i<n;i++)
{
scanf("%d%d%d",&a[i].l,&a[i].r,&a[i].d);
pre[i]=i;size[i]=1;
ans+=a[i].d;
}
pre[n]=n;size[n]=1;
sort(a+1,a+n,cmp);//n-1条边
for(int i=1;i<n;i++)
{
int x=findd(a[i].l);
int y=findd(a[i].r);
if(x!=y)
{
ans+=(ll)(size[x]*size[y]-1)*(a[i].d+1);//这里!!!wa了好几次的起源...
pre[x]=y;
size[y]+=size[x];
}
}
printf("%lld\n",ans);
return 0;
}