常用的两种算法:Prim算法和Kruskal算法
这两种算法都是基于贪心的思想,两种算法最大的不同在于:Prim算法需要建图,使用邻接矩阵更为方便;Kruskal算法无需建图,只需用并查集找到各个点之间的关系
易错点!!!:
1.并查集不要忘记初始化数组!!!
2.用Kruskal算法如果有特殊的跳出条件(即条件要求最后生成树的边数不为n-1)要用break跳出
题目描述
如题,给出一个无向图,求出最小生成树,如果该图不连通,则输出 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。
样例解释:
所以最小生成树的总边权为2+2+3=7。
1.Prim算法(体现出整体思想)
又称为加边法,即每次选择最小权值的边加入到生成树中,然后再更新权值,如此反复,保证每次最优来达到最优解。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int g[5001][5001],Min[5001]; //g[][]存放邻接矩阵
// Min[i]存放未加入最小生成树的点 与 任意已加入生成树的点的路径的权值的最小值
bool check[5001]; // check[i]=true表示i已加入生成树
int main()
{
int n,m;
cin>>n>>m;
memset(g,0x3f3f3f3f,sizeof(g));
memset(Min,0x3f3f3f3f,sizeof(Min));
// 初始化为最大值
for(int i=1;i<=m;++i)
{
int x,y,z;
cin>>x>>y>>z;
if(z<g[x][y])
g[x][y]=g[y][x]=z; //无向图输入
}
Min[1]=0; // 起点赋0值,即默认从起点开始
for(int i=1;i<=n;++i)
{
int num=0;
for(int j=1;j<=n;++j)
{
if(!check[j]&&(Min[j]<Min[num]))
{
num=j;
}
}
check[num]=true; // num加入最小生成树
if(num==0)
// 整个过程一直没有找到路径,但是点还没有加入完,说明图不连通
{
cout<<"orz"<<endl;
return 0;
}
for(int j=1;j<=n;++j)
// 未加入最小生成树且与新加入num点相邻的点,通过g[num][j]更新Min[j]
{
if(!check[j]&&(g[num][j]<Min[j]))
{
Min[j]=g[num][j];
}
}
}
long long int sum=0;
for(int i=1;i<=n;++i)
{
sum+=Min[i];
}
cout<<sum<<endl;
return 0;
}
2.Kruskal算法 (该算法不需要建图,更好写)
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <algorithm>
using namespace std;
int n,m;
const int maxn=1e7+5;
struct node{
int u,v,w;
}edge[maxn<<1];
// Kruskal算法不需要建图,只需将点输入并存起来即可
int fa[maxn];
inline int read()
{
int x=0,f=1;
char c=getchar();
while(c<'0'||c>'9')
{
if(c=='-') f=-1;
c=getchar();
}
while(c>='0'&&c<='9')
{
x=(x<<3)+(x<<1)+(c^48);
c=getchar();
}
return x*f;
}
bool cmp(node x,node y) //根据边权从小到大进行排序
{
return x.w<y.w;
}
inline int find(int x) //并查集的查找
{
if(x!=fa[x]) fa[x]=find(fa[x]);
return fa[x];
}
inline void kruskal() //kruskal算法的核心
{
int ans=0,cnt=0;
for(int i=1;i<=m;++i)
{
int p=find(edge[i].u);
int q=find(edge[i].v);
//找到两个点分别属于哪个集合,换句话说就是u和v分别属于哪个根节点下的子节点
if(p==q) continue; //如果两个点属于一个集合,进行下一次循环
ans+=edge[i].w; //统计最小生成树的边权之和
fa[q]=p; //将两点合并为一个集合
++cnt; //统计树的边数
}
if(cnt==n-1) //当边全部搜索过且等于n-1说明最小生成树已生成
{
cout<<ans<<endl;
}
else cout<<"orz"<<endl; //否则不存在
}
int main()
{
n=read();m=read();
for(int i=1;i<=n;++i)
{
fa[i]=i; //并查集初始化
}
for(int i=1;i<=m;++i)
{
edge[i].u=read();
edge[i].v=read();
edge[i].w=read();
}
sort(edge+1,edge+m+1,cmp); //注意排序的是边,注意中间的范围
kruskal();
return 0;
}
/*特判数据:
输入:
5 6
1 2 3
2 3 4
3 1 4
4 5 6
5 4 6
1 3 5
输出:
orz
*/
相关练习:
1.洛谷 P2872 [USACO07DEC]Building Roads S
题目描述
给定 n 个点的坐标,第 i 个点的坐标为 (x_i,y_i),这 n 个点编号为 1 到 n。给定 m 条边,第 i 条边连接第 u_i 个点和第 v_i 个点。现在要求你添加一些边,并且能使得任意一点都可以连通其他所有点。求添加的边的总长度的最小值。
输入格式
第一行两个整数 n,m 代表点数与边数。
接下来 n 行每行两个整数 x_i,y_i 代表第 i 个点的坐标。
接下来 m 行每行两个整数 u_i,v_i 代表第 i 条边连接第 u_i 个点和第 v_i 个点。
输出格式
一行一个实数代表添加的边的最小长度,要求保留两位小数,为了避免误差, 请用 64 位实型变量进行计算。
输入输出样例
输入 #1
4 1 1 1 3 1 2 3 4 3 1 4
输出 #1
4.00
说明/提示
数据规模与约定
对于 100% 的整数,1≤n,m≤10001≤x_i,y_i≤10^6,1≤u_i,v_i≤n。
已AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <algorithm>
using namespace std;
int n,m;
const int maxn=1e7+5;
int x[10005],y[10005];
struct node{
int x,y;
double dis;
}edge[maxn<<1];
double ans=0.0;
int num=0,cnt=0,fa[maxn];
inline int read()
{
int x=0,f=1;
char c=getchar();
while(c<'0'||c>'9')
{
if(c=='-') f=-1;
c=getchar();
}
while(c>='0'&&c<='9')
{
x=(x<<3)+(x<<1)+(c^48);
c=getchar();
}
return x*f;
}
inline void turn()
{
for(int i=1;i<=n-1;++i)
{
for(int j=i+1;j<=n;++j)
{
edge[++num].dis=sqrt((double)(x[i]-x[j])*(double)(x[i]-x[j])+(double)(y[i]-y[j])*(double)(y[i]-y[j])); //在sqrt()函数中用double强制转换
edge[num].x=i;
edge[num].y=j;
}
}
}
bool cmp(node x,node y)
{
return x.dis<y.dis;
}
inline int find(int x)
{
if(x==fa[x]) return x;
return fa[x]=find(fa[x]);
}
inline void kruskal() //kruskal算法模板
{
for(int i=1;i<=num;++i)
{
int p=find(edge[i].x);
int q=find(edge[i].y);
if(p==q) continue;
ans+=edge[i].dis;
fa[q]=p;
++cnt;
if(cnt==n-1) break;
}
printf("%.2lf\n",ans);
}
int main()
{
n=read(),m=read();
for(int i=1;i<=n;++i)
{
fa[i]=i;
}
for(int i=1;i<=n;++i)
{
x[i]=read();
y[i]=read();
}
turn(); //将点与点的距离存进struct内
for(int i=1;i<=m;++i)
{
int u=read(),v=read();
edge[++num].x=u;
edge[num].y=v;
edge[num].dis=0.0; //题目要求一定要建立的路,将距离设置为0,排序会排在最前面
}
sort(edge+1,edge+num+1,cmp);
kruskal();
return 0;
}
思路:因要求被除去的网线值最大,说明能联通各个节点且无重复路径的路要最小,即求最小生成树,用原路径总长减去最小生成树大小即可。
已AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <algorithm>
using namespace std;
int n,k;
const int maxn=1e7+5;
struct node{
int i,j,m;
}edge[maxn<<1];
int ans=0,cnt=0,sum=0,fa[maxn];
inline int read()
{
int x=0,f=1;
char c=getchar();
while(c<'0'||c>'9')
{
if(c=='-') f=-1;
c=getchar();
}
while(c>='0'&&c<='9')
{
x=(x<<3)+(x<<1)+(c^48);
c=getchar();
}
return x*f;
}
bool cmp(node x,node y)
{
return x.m<y.m;
}
inline int find(int x)
{
if(x==fa[x]) return x;
return fa[x]=find(fa[x]);
}
inline void kruskal()
{
for(int i=1;i<=k;++i)
{
int p=find(edge[i].i);
int q=find(edge[i].j);
if(p==q) continue;
ans+=edge[i].m; //统计最小生成树的长度
fa[q]=p;
++cnt;
if(cnt==k-1) break;
}
}
int main()
{
n=read();k=read();
for(int i=1;i<=n;++i)
{
fa[i]=i;
}
for(int i=1;i<=k;++i)
{
edge[i].i=read();
edge[i].j=read();
edge[i].m=read();
sum+=edge[i].m; //统计原来路径总长
}
sort(edge+1,edge+k+1,cmp);
kruskal();
printf("%d",sum-ans);
return 0;
}
思路:
1. 题目:
卫星电话(隐含意思):可以使某两点之间的距离变为0,因为有 s 个点有卫星电话,这就意味着在最好的情况下,会有 s−1 条边的边权为 0,那么在p-1条边中有s-1条边为0,通过最小生成树使得不为0的边最小,即将此问题转为:求边只有p-s个的最小生成树中的最大边权
最小通话距离 D:所构成的min代价联通路中,求边权的max
进一步思考:边权越小,最好近点相连,即:放弃掉跨越中间点的连接方式,能直接到达的就直接到达,再远一点的用卫星电话。
已AC代码:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <algorithm>
#include <cmath>
using namespace std;
const int maxn=1e7+5;
int num=0,cnt=0;
double ans;
struct node{
int u,v;
double w;
}edge[maxn<<1];
int x[maxn],y[maxn],fa[maxn],p,s;
inline int read()
{
int x=0,f=1;
char c=getchar();
while(c<'0'||c>'9')
{
if(c=='-') f=-1;
c=getchar();
}
while(c>='0'&&c<='9')
{
x=(x<<3)+(x<<1)+(c^48);
c=getchar();
}
return x*f;
}
inline void dis()
{
for(int i=1;i<=p-1;++i)
{
for(int j=i+1;j<=p;++j)
{
edge[++num].w=sqrt((double)(x[i]-x[j])*(double)(x[i]-x[j])+(double)(y[i]-y[j])*(double)(y[i]-y[j]));
edge[num].u=i;
edge[num].v=j;
}
}
}
bool cmp(node x,node y)
{
return x.w<y.w;
}
inline int find(int x)
{
if(x==fa[x]) return x;
return fa[x]=find(fa[x]);
}
inline void kruskal()
{
for(int i=1;i<=num;++i)
{
int n=find(edge[i].u);
int m=find(edge[i].v);
if(n==m) continue;
ans=max(ans,edge[i].w);
//从小到大进行存储、比较,当边数cnt等于p-s时,即可获得最大距离
++cnt;
fa[m]=n;
if(cnt==p-s) break;
}
printf("%.2lf",ans);
}
int main()
{
s=read();p=read();
for(int i=1;i<=p;++i)
{
fa[i]=i;
}
for(int i=1;i<=p;++i)
{
x[i]=read();
y[i]=read();
}
dis();
sort(edge+1,edge+num+1,cmp);
kruskal();
return 0;
}