自己实现的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 */