您的位置:首页 > 其它

Kruskal算法求最小生成树 附hdoj1162

2013-05-14 12:04 330 查看
自己实现的kruskal算法。

其实懂了并查集,实现kurskal算法便很简单了。

按照克鲁斯卡尔算法,从最小的边开始选(所以把所有边按权值非降序排序),然后选的过程一定是从小到大地选,如果发现不适合的就抛弃,而且绝对不会回溯再次检查是否适合(不吃回头艹)。因此如何判断边适合就是算法的关键了。

这里的关键其实就是并查集,并查集是用集合的观点来看结点与结点的关系。结点作为元素,只有属于和不属于某个集合。当日,集合要求元素不能重复。

集合内点与点的关系只有:同属于一个集合。图或树中的点,它们之间的关系很多,什么左孩子右孩子,爷爷孙子等等。但在集合中,这些都不需要考虑。

并查集其实就是带有快速合并查询功能的动态集合。

建立并查集,想象为图或树的每个元素画一个圈,即每个元素分别看成一个集合,同时也看成一棵树,而树根就是自己,树根的标志的结点的父母是自己,因此初始化为par[i] = i

然后图或树中两个元素合并,合并前会查询是否同属于一个集合(查询就是看树根是否一样,刚开始肯定都不一样),是则不理睬,否则合并,合并的时候把其中一棵树作为另一棵树的孩子。

假如查询前,树的深度很大,形状几乎是线性的,那么查询的时候,递归压栈完毕后就找到了树根,那么回溯(出栈)时,每一层的子树的树根都直接修改指向树根。这样下次查询时就是O(1)的效率了(这里被称作路径压缩)。查询的效率实际测试是非常高的,但理论分析需要作平摊分析,这个我还没搞懂。

搞懂并查集后就好办了。

选择适合的边,即这条边加入后树不能出现环路(否则就不是树了),其实就是说这条边的两个端点不能属于同一个非空集合,而一个并查集是非空的,而且是一棵树(现在的形状是连通的,原来的形状也一定是连通的,只是集合中不再关系如何连接,只关心 是否 连通)。若边x的两个端点a, b属于同一个非空集合c(代表元素是c),x加入后,c既连通了a,又连通了b,而ab本身是连通的,那么就形成了环。

建立并查集后,没有空集。因此,选择的条件就是:两个端点是否属于同一个集合,是则不需要合并,也不能选择,否则选择这条边,并且合并为一个集合。可见,选择是伴随集合的动态变化而变化的。

#include <iostream>
#include <algorithm>
using namespace std;
const int MAXN = 10000;

struct edge {
int a, b;
int weight;
bool operator < (const edge& cur) const
{
return weight < cur.weight;
}
};

int par[MAXN];
edge e[MAXN];
edge MST[MAXN];

void makeSet(int n)
{
for (int i = 0; i != n; i++) {
par[i] = i;
}
}

int getPar(int i)
{
if (par[i] != i) {
par[i] = getPar(par[i]);
}
return par[i];
}

//假定可以合并
//可以合并则合并并且返回真,不能合并则返回假,
bool unionSet(int farther, int son)
{
int f = getPar(farther);
int s = getPar(son);
if (f == s)
return false;
par[s] = f;
return true;
}

//结点0打印为结点A
//仅提供主函数在结点数目很小的情况下使用,方便检查边
char trans(int i)
{
return i+'A';
}

int main()
{
int n, m; //点数 边数
int k = 0;
int weight_sum = 0;

cin >> n >> m;

//假定输出无向图的边,其边数为2*m
for (int i = 0; i != 2*m; i++) {
cin >> e[i].a >> e[i].b >> e[i].weight;
}

makeSet(n);
sort(e, e+2*m);

for (int i = 0; i != 2*m; i++) {
if (unionSet(e[i].a, e[i].b)) {
MST[k++] = e[i];
weight_sum += e[i].weight;
}
}

//输出MST
for (int i = 0; i != k; i++) {
cout << trans(MST[i].a) << "->" << trans(MST[i].b) << " "
<< "weight = " << MST[i].weight << endl;
}
cout << "weight_sum = " << weight_sum << endl;
return 0;
}

/*
测试数据参考

点数+边数
两端点+边权值
//图片请参考 百度百科kruskal的图片

7 11
0 1 7
0 3 5
1 0 7
1 2 8
1 3 9
1 4 7
2 1 8
2 4 5
3 0 5
3 1 9
3 4 15
3 5 6
4 1 7
4 2 5
4 3 15
4 5 8
4 6 9
5 3 6
5 4 8
5 6 11
6 4 9
6 5 11

对应输出:
A->D weight = 5
E->C weight = 5
F->D weight = 6
E->B weight = 7
B->A weight = 7
E->G weight = 9
weight_sum = 39

*/


上面测试数据对应的图为:



为了验证代码的正确性,找个简单的题目测试一下 hdoj1162

这个题目用Prim算法可能更加适合,但数据量很小,根据n个点建立(n(n-1))/2条边也不超过5000条,虽然是稠密图,但用卡鲁斯卡尔算法也没问题

AC代码如下:

#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstdio>
using namespace std;
const int MAXN = 110;

//点结构
struct point {
double x, y;
};

//边结构 并重载操作符<
struct edge {
double a, b;
double weight;
bool operator < (const edge& cur) const
{
return weight < cur.weight;
}
};

point p[MAXN];
edge e[MAXN*MAXN/2];
int par[MAXN];

void makeSet(int n)
{
for (int i = 0; i != n; i++) {
par[i] = i;
}
}

int getPar(int i)
{
if (par[i] != i) {
par[i] = getPar(par[i]);
}
return par[i];
}

//假定可以合并
//可以合并则合并并且返回真,不能合并则返回假,
bool unionSet(int farther, int son)
{
int f = getPar(farther);
int s = getPar(son);
if (f == s)
return false;
par[s] = f;
return true;
}

inline double dist(double x1, double x2, double y1, double y2)
{
return sqrt( (x1-x2)*(x1-x2)+(y1-y2)*(y1-y2) );
}

int main()
{
int n;
while (cin >> n) {
int cnt = 0;
double weight_sum = 0.0;

//input point
for (int i = 0; i != n; i++) {
cin >> p[i].x >> p[i].y;
}
//make edge by points, cnt will equal to (n*(n-1))/2
for (int i = 0; i != n; i++) {
for (int j = i+1; j != n; j++) {
e[cnt].a = i;
e[cnt].b = j;
e[cnt].weight = dist(p[i].x, p[j].x, p[i].y, p[j].y);
++cnt;
}
}
makeSet(n);
sort(e, e+cnt);
//kruskal algorithm
for (int i = 0; i != cnt; i++) {
if (unionSet(e[i].a, e[i].b)) {
weight_sum += e[i].weight;
}
}
printf("%.2lf\n", weight_sum);
}
return 0;
}

/*
参考测试数据
3
1.0 1.0
2.0 2.0
2.0 4.0

3
1.0 1.0
2.0 2.0
3.0 3.0

输出为
3.41
2.83
*/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: