【算法总结】图论-最小生成树
一、概念
在一个无向连通图中,如果存在一个连通子图包含原图中所有的结点和部分边,且这个子图不存在回路,那么我们称这个子图为原图的一棵生成树。在带权图中,所有的生成树中边权的和最小的那棵(或几棵)被称为最小生成树。
定理:在要求解的连通图中,任意选择一些点属于集合A,剩余的点属于集合B, 必定存在一棵最小生成树包含两个顶点分别属于集合 A 和集合 B 的边(即连通两个集合的边)中权值最小的边。
二、Kruskal算法
1.初始时所有结点属于孤立的集合。
2.按照边权递增顺序遍历所有的边,若遍历到的边两个顶点仍分属不同的集合(该边即为连通这两个集合的边中权值最小的那条)则确定该边为最小生成树上的一条边,并将这两个顶点分属的集合合并。
3.遍历完所有边后,原图上所有结点属于同一个集合则被选取的边和原图中所有结点构成最小生成树;否则原图不连通,最小生成树不存在。
如步骤所示,在用Kruskal算法求解最小生成树的过程中涉及到大量的集合操作,我们恰好可以使用上一节中讨论的并查集来实现这些操作。
例 5.3 还是畅通工程
解题思路
在给定的道路中选取一些,使所有的城市直接或间接连通且使道路的总长度最小,该例即为典型的最小生成树问题。我们将城市抽象成图上的结点,将道路抽象成连接点的边,其长度即为边的权值。经过这样的抽象,我们求得该图的最小生成树,其上所有的边权和即为所求。 注意城市编号是从1开始的,由于p[i]=i,所以遍历也要从1开始。
AC代码

#include<cstdio> #include<algorithm> using namespace std; const int 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);//起始元素为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 ", ans); } return 0; }
例 5.4 Freckles
解题思路
题目大意为平面上有若干个点,我们需要用一些线段来将这些点连接起来使任意两个点能够通过一系列的线段相连,给出所有点的坐标,求一种连接方式使所有线段的长度和最小,求该长度和。
若我们将平面上的点抽象成图上的结点,将结点间直接相邻的线段抽象成连接结点的边,且权值为其长度,那么该类似于几何最优值的问题就被我们转化到了图论上的最小生成树问题。但在开始求最小生成树前,我们必须先建立该图,得出所有的边和相应的权值。
AC代码

#include<cstdio> #include<cmath> #include<algorithm> using namespace std; const int 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;//最小生成树上边权和初始化为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 ", ans); } return 0; }