前言
本篇文章主要讲述我在完成CCF201812-4数据中心这道题时的一些思考过程和回顾总结。
题目
思路
通过题目所给出的样例说明可以分析出来,本题似乎是要寻找那么一棵树,怎么样的一棵树呢,由图例知道Ti即为第i层的最大边权,然后再找出最大的Ti,而且是一个时间花费最少的树,毫无疑问题目即为给你一个图,找出由这个图生成的最小生成树。
明确思路以后就是使用相关的算法来实现,而我们常用来寻找最小生成树的算法为prim和kruskal,那么此处该如何选择呢,我们知道prim适用于顶点少,而边多的情况,反之顶点多边少的就应该选择kruskal算法,首先说明我最初的想法,在看到这个题的子任务后,如上图所示,发现前面几个子任务确实是边比顶点多,于是我选择了prim算法,但实现完提交之后我只能得到前两个子任务的分,因为后面的任务3和4超时,所以应该换kruskal算法。此处先贴出我prim版的代码,后文会继续讲述以后碰到这种题怎么能一眼选出相对更适合的算法并会贴出kruskal版的代码。
Prim代码如下
注意!此为prim版,C++提交50分(完成前两个任务)的代码。
#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXV = 10010;
const int INF = 1000000000;
int n,m,root,maxG = 0;
int G[MAXV][MAXV];
int d[MAXV];
bool vis[MAXV] = {false};
int prim(int root){
fill(d,d+MAXV,INF);
d[root] = 0;
for(int i = 1;i<=n;i++){
int u = -1,MIN = INF;
for(int j = 1;j<=n;j++){
if(vis[j]==false&&d[j]<MIN){
u = j;
MIN = d[j];
}
}
if(u == -1) return -1;
vis[u] = true;
if(d[u]>maxG) maxG = d[u];
for(int v = 1;v<=n;v++){
if(vis[v] == false&&G[u][v]!=INF&&G[u][v]<d[v]){
d[v] = G[u][v];
}
}
}
return maxG;
}
int main(){
int u,v,t;
scanf("%d",&n);
scanf("%d",&m);
scanf("%d",&root);
fill(G[0],G[0]+MAXV*MAXV,INF);
for(int i = 0;i<m;i++){
scanf("%d%d%d",&u,&v,&t);
G[u][v] = G[v][u] = t;
}
int ans = prim(root);
printf("%d\n",ans);
return 0;
}
持续更新
如前文所述,当使用prim算法实现时会导致数据量大时出现超时和超内存的情况,通过分析代码可以发现主要在于程序会建立一个非常大的邻接矩阵用来表示图,从而容易超过规定的空间大小,其次,时间复杂度为O(V^2)级,当顶点过多时,时间上并不允许。而kruskal就不会,kruskal主要的思想是在诸多边里,按边的大小顺序排序,再遍历所有边将其考虑是否加入最小生成树。最后实现的代码通过了测试。
Kruskal实现代码如下
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int MAXV = 500010;
int n,m,root,maxE = 0;
struct edge{
int u,v;
int cost;
}E[MAXV];
bool cmp (edge a,edge b){
return a.cost<b.cost;
}
int father[MAXV];
int findFather(int x){
int a = x;
while(x!=father[x]){
x = father[x];
}
//路径压缩
while(a!=father[a]) {
int z = a;
a = father[a];
father[z] = x;
}
return x;
}
int kruskal(int n,int m){
int num_edge = 0;
for(int i = 1;i<=n;i++) father[i] = i;
sort(E,E+m,cmp);
for(int i = 0;i<m;i++){
int faU = findFather(E[i].u);
int faV = findFather(E[i].v);
if(faU!=faV){
father[faV] = faU;
if(E[i].cost>maxE) maxE = E[i].cost;
num_edge++;
if(num_edge==n-1) break;
}
}
if(num_edge!= n-1) return -1;
else return maxE;
}
int main(){
scanf("%d",&n);
scanf("%d",&m);
scanf("%d",&root);
for(int i = 0;i<m;i++){
scanf("%d%d%d",&E[i].u,&E[i].v,&E[i].cost);
}
int ans = kruskal(n,m);
printf("%d\n",ans);
return 0;
}
总结
从最小生成树角度说,我们常用的两种算法,在考试应用中,kruskal通常会比prim拥有更好的性能,当然,使用prim算法时也可以堆优化,不过个人觉得还是kruskal来得更直接,用来应付考试也已经足够。