基于在《图论及其应用——图》中对于并查集简单的了解,这里我们将继续讨论一下关于“树”的问题。首先这里引入一些概念。
所谓生成树,简单的来说就是对于连通分量为1的Graph图——G(V,E),我们找到一个G的子图T(V',E'),这里V = V',E' 则可以是E的子集。
那对应的生成森林,就是连通分量为n的Graph图,我们分别分析每一个连通分量,分别得到生成树,最后组在一起就成了生成森林。
而所谓的最小生成树,就是如果给定的Graph图——G(V,E)的点集E是带权值的(即这个边上存着数值),在所有生成树中,权值最小的那个即是最小生成树。
以上的定义显得很抽象,我们结合具体的问题来看就会更加明白。
那我们就拿来一个具体的例子来分析。(Problem source : hdu 1301)
题目大意:通过键盘输入给你一个Graph图——G(V,E),其中边集E是带权值的(就说连接点之间的边上储存着数值),现在你找到一个子图,要求这个子图能够连通原图中的所有点(也就是说这个图首先是原图的生成树),并且这个生成树的权值是所有生成树中最小的,即最小生成树。 数理分析:关于生成最小生成树的算法,常见的有prim算法和Kruskal算法,这里我们将介绍Kruskal算法。
它的具体步骤如下。
1.我们设G(V,E)是原图(点集V含有n个元素),G'(V',E')是最小生成树。我们先将带权值的边集E进行排序。
2.我们先生成G'(V,∅),然后从权值最小的边开始尝试构建最小生成树G',如果添加当前这条最小边导致生成了环,则弃掉当前边。
3.遍历所有的边,如果能够选出n - 1条,则说明得到了最小生成树,否则的话,只能生成最小森林。
Kruskal算法基于贪心的思想。为了得到生成树,我们要在边集E中选出n - 1个元素,这是保证含有n个元素的点集连通的最小边数,那么我们只需从最小的带权边开始构造即可。这个构造过程中,如果出现了某个边使生成树出现了环,那么我们至少需要n条边才能构造出生成树(这显而易见)。此时我们有两个选择,一种选择是,弃掉这条边,我们构造n-1个元素的边集V',另一种选择是,拿这条边,构造n个元素的边集V'。我们简单的进行比较就能够的出答案。 E = {E1,E2,E3,E4,E5,E6}(按权值的递增排列)这里我们假设E3处生成环。
方案一:E1,E2,E4,E5。
方案二:E1,E2,E3,E4,E5,E6.(因为选择了导致构成环的E3对生成树的连通性没有影响,所以E3后面的点和方案一完全一样,但是要多一个元素) 显而易见,方案一才是运用了贪心思想的最优构造方法。 以上就是生成最小生成树的Kruskal算法。
编程实现:有了以上对Kruskal算法的理解再加上对并查集的理解,我们在编程实现上并不难实现它。 值得一提的是,在并查集和最小生成树一类的问题当中,往往是把本质上是无向的Graph图当做有向图来处理,这里主要是为了编程实现的方便。
参考代码如下。
#include<stdio.h> #include<string.h> using namespace std; int root[30]; int Find(int t) { if(root[t] == t) return t; else return root[t] = Find(root[t]); } void bing(int x,int y) { if(Find(x) != Find(y)) { if(Find(x) < Find(y)) root[Find(y)] = Find(x); else root[Find(x)] = Find(y); } } int main() { int n,i,j,k,x,y,Min,sum,cost[30][30]; char ch1,ch2; while(scanf("%d",&n) , n) { getchar(); memset(cost , -1 , sizeof(cost)); for(i = 1;i < n;i++) { root[i] = i; scanf("%c %d",&ch1,&k); for(j = 1;j <= k;j++) { scanf(" %c %d",&ch2,&x); cost[i][ch2 - 'A' + 1] = x; } getchar(); } root[n] = n; sum = 0; for(k = 1;k < n;k++) { Min = 100000; for(i = 1;i < n;i++) { for(j = i + 1;j <= n;j++) { if(cost[i][j] > 0 && Find(i) != Find(j) && cost[i][j] < Min) { Min = cost[i][j]; x = i , y = j; } } } bing(x , y); sum += Min; } printf("%d ",sum); } return 0; }
我们不妨再看一道关于最小生成树的题目。(Problem source : hdu1233)
编程实现:还是典型的最小生成树问题,我们依然用Kruskal算法来实现,但是我们这次基于结构体来记录Graph图的信息。
我们构造带权边集E的结构体,含有三个变量,分别是这条边的起点preV,终点endV,权重weigh。这样用边集E记录图的好处在于,对边的权进行排序非常方便。
随后我们只需按照Kruskal算法给出的步骤编程实现即可。
参考代码如下。
#include<stdio.h> #include<algorithm> using namespace std; const int maxn1 = 5000; const int maxn2 = 105; int father[maxn2]; struct edge { int weigh; int preV; int endV; }edges[maxn1]; bool cmp(edge a , edge b) { return a.weigh < b.weigh ? 1 : 0; } int Find(int x) { if(father[x] == x) return x; else return father[x] = Find(father[x]); } int Kruskal(int n , int es) { int sum = 0; for(int i = 1;i <= n;i++) father[i] = i; sort(edges+1,edges + 1 + es,cmp); for(int i = 1;i <= es;i++) { int fx = Find(edges[i].preV); int fy = Find(edges[i].endV); if(fx != fy) { father[fx] = fy; sum += edges[i].weigh; } } return sum; } int main() { int n; int i , es; while(scanf("%d",&n) , n) { es = (n-1)*n*0.5; for(i = 1;i <= es;i++) { scanf("%d %d %d",&edges[i].preV,&edges[i].endV,&edges[i].weigh); } int ans = Kruskal(n,es); printf("%d ",ans); } }