算例一
-
题目描述
-
解题思路
①最小生成树:在一个无向连通图中,如果存在一个连通子图包含原图中所有的结点和部分边,且这个子图不存在回路,那么我们称这个子图为原图的一棵生成树。在带权图中,所有的生成树中边权的和最小的那棵(或几棵)被称为最小生成树
②定理:在要求解的连通图中,任意选择一些点属于集合A,剩余的点属于集合B,必定存在一棵最小生成树包含两个顶点分别属于集合A 和集合B 的边(即连通两个集合的边)中权值最小的边。可以用反证法证明,假设AB有一条权重最小的边E,且该边不在所有最小生成树中,则最小生成树中定有一条连通AB的边(若没有则成不连通图,若大于一条则出现回路),且该边权重是大于E的,那么用E替换此边,会得到更小的最小生成树,与假设矛盾
③Kruskal (克鲁斯卡尔)算法原理:
(1)初始时所有结点属于孤立的集合
(2)按照边权递增顺序遍历所有的边,若遍历到的边两个顶点仍分属不同的集合(该边即为连通这两个集合的边中权值最小的那条)则确定该边为最小生成树上的一条边,并将这两个顶点分属的集合合并
(3)遍历完所有边后,原图上所有结点属于同一个集合则被选取的边和原图中所有结点构成最小生成树;否则原图不连通,最小生成树不存在。
-
解题代码
#include <stdio.h>
#include <algorithm>
using namespace std;
#define N 101
int Tree[N];
int findRoot(int x) //查找代表集合的树的根结点
{
if (Tree[x] == -1)
return x;
else
{
int tmp = findRoot(Tree[x]);
Tree[x] = tmp;
return tmp;
}
}
struct Edge //边结构体
{
int a, b; //边两个顶点的编号
int cost; //该边的权值
bool operator < ( const Edge & A) const //重载小于号使其可以按照边权从小到大排列
{
return cost < A.cost;
}
} edge[6000];
int main ()
{
int n;
while (scanf ("%d",& n) != EOF && n != 0)
{
for (int i = 1; i <= n * ( n - 1) / 2; i ++)
{
scanf ("%d%d%d",& edge[i].a,& edge[ i].b,& edge[i].cost);
} //输入
sort(edge + 1,edge + 1 + n * ( n - 1) / 2); //按照边权值递增排列所有的边
for (int i = 1; i <= n; i ++)
Tree[i] = -1; //初始时所有的结点都属于孤立的集合
int ans = 0; //最小生成树上边权的和,初始值为0
for (int i = 1; i <= n * ( n - 1) / 2; i ++) //按照边权值递增顺序遍历所有的边
{
int a = findRoot(edge[i].a);
int b = findRoot(edge[i].b); //查找该边两个顶点的集合信息
if (a != b) //若它们属于不同集合,则选用该边
{
Tree[a] = b; //合并两个集合
ans += edge[ i].cost; //累加该边权值
}
}
printf ("%d\n",ans); //输出
}
return 0;
}
-
注意点
①上述代码中,并没有对所有城市是否在同一个连通图中做判断,因为本题不会出现在这种情况,也可以尝试做这样的检验操作,我的做法是,定义一个cost[i]的数组记录第i号城市作为根节点时的连通图总权重,最后遍历所有点,若根节点有两个及以上,则说明没有最小生成树,否则取那个根节点的总权重即可
算例二【Freckles(九度OJ 1144)】
-
题目描述
-
解题思路
①建立图的结构,需要自己计算节点的距离,然后求最小生成树即可
-
解题代码
#include <stdio.h>
#include <math.h>
#include <algorithm>
using namespace std;
#define N 101
int Tree[N];
int findRoot(int x)
{
if (Tree[x] == -1)
return x;
else
{
int tmp = findRoot(Tree[x]);
Tree[x] = tmp;
return tmp;
}
}
struct Edge
{
int a, b;
double cost; //权值变为长度,固改用浮点数
bool operator < ( const Edge & A) const
{
return cost < A.cost;
}
} edge[6000];
struct Point //点结构体
{
double x, y; //点的两个坐标值
double getDistance(Point A) //计算点之间的距离
{
double tmp = (x - A.x) * ( x - A.x) + ( y - A.y) * ( y - A.y);
return sqrt(tmp);
}
} list[101];
int main ()
{
int n;
while (scanf ("%d",& n) != EOF )
{
for (int i = 1; i <= n; i ++)
{
scanf ("%lf%lf",& list[ i].x,& list[ i].y);
} //输入
int size = 0; //抽象出的边的总数
for (int i = 1; i <= n; i ++)
{
for (int j = i + 1; j <= n; j ++) //连接两点的线段抽象成边
{
edge[size].a = i;
edge[size].b = j ; //该边的两个顶点编号
edge[size].cost = list[i].getDistance(list[j]); //边权值为两点之间的长度
size ++; //边的总数增加
} //遍历所有的点对
}
sort(edge,edge + size); //对边按权值递增排序
for (int i = 1; i <= n; i ++)
{
Tree[i] = -1;
}
double ans = 0;
for (int i = 0; i < size; i ++)
{
int a = findRoot(edge[i].a);
int b = findRoot(edge[i].b);
if (a != b)
{
Tree[a] = b;
ans += edge[ i].cost;
}
} //最小生成树
printf ( "%.2lf\n",ans); //输出
}
return 0;
}
-
注意点
①关于最后的权值相加的问题,我之前有担心说,如果两个图没连通那加的权值却全在一起,怪怪的。。。所以我加了个判断是否已经连成连通图的条件,如果是就直接结束,例如:
bool IsEnd(int n){
int t=0;
for(int i=0;i<n;i++){
if(Tree[i]==-1)
t++;
}
if(t==1)return true;
else return false;
}
//.........
if(a!=b){
Tree[edge[i].a]=b;
ans +=edge[i].cost;
if(IsEnd(n)){
printf("%.2f\n",ans);
break;
}
}
而优秀代码是直接进行for循环到底,若已连通则不加权值,未连通则加权值,,,后来仔细一想,遍历所有,最终一定会成一个连通图,而又是从小到大进行判断、权值累加,所以当连通图形成之后,自然也不会去累加其他乱七八糟的权值,,,恩是我想多了!(和上题的有点相似,上题本应该进行判断是否连成一个连通图,但是因为输入了所有可能的边权值,故不存在不成连通图的情况,emmmm总结就是,如果所有点的相互权值都知道,即一定可以形成连通图,则直接for循环到底即可!)