第六节 最小生成树
引入 一、什么是图的最小生成树(MST)? 不知道大家还记不记得树的一个定理:N个点用N-1条边连接成一个连通块,形成的图形只可能是树,没有别的可能。 一个有N个点的图,边一定是大于等于N-1条的。图的最小生成树,就是在这些边中选择N-1条出来,连接所有的N个点。这N-1条边的边权之和是所有方案中最小的。
引入 二、最小生成树用来解决什么问题? 就是用来解决如何用最小的“代价”用N-1条边连接N个点的问题。例如: 【例4-9】、城市公交网建设问题 【问题描述】 有一张城市地图,图中的顶点为城市,无向边代表两个城市间的连通关系,边上的权为在这两个城市之间修建高速公路的造价,研究后发现,这个地图有一个特点,即任一对城市都是连通的。现在的问题是,要修建若干高速公路把所有城市联系起来,问如何设计可使得工程的总造价最少? 【输入格式】 n(城市数,1<=n<=100) e(边数) 以下e行,每行3个数i,j,wij,表示在城市i,j之间修建高速公路的造价。 【输出格式】 n-1行,每行为两个城市的序号,表明这两个城市间建一条高速公路。
引入 【输入样例】 5 8 1 2 2 2 5 9 5 4 7 4 1 10 1 3 12 4 3 6 5 3 3 2 3 8 【输出样例】 1 2 2 3 3 4 3 5
Prim算法 Prim算法采用与Dijkstra、Bellman-Ford算法一样的“蓝白点”思想:白点代表已经进入最小生成树的点,蓝点代表未进入最小生成树的点。 算法描述: 以1为起点生成最小生成树,min[v]表示蓝点v与白点相连的最小边权。 MST表示最小生成树的权值之和。 a)初始化:min[v]= ∞(v≠1); min[1]=0;MST=0; b)for (i = 1; i<= n; i++) 1.寻找min[u]最小的蓝点u。 2.将u标记为白点 3.MST+=min[u] 4.for 与白点u相连的所有蓝点v if (w[u][v]<min[v]) min[v]=w[u][v]; c)算法结束: MST即为最小生成树的权值之和 算法分析&思想讲解: Prim算法每次循环都将一个蓝点u变为白点,并且此蓝点u与白点相连的最小边权min[u]还是当前所有蓝点中最小的。这样相当于向生成树中添加了n-1次最小的边,最后得到的一定是最小生成树。 我们通过对下图最小生成树的求解模拟来理解上面的思想。蓝点和虚线代表未进入最小生成树的点、边;白点和实线代表已进入最小生成树的点、边。
Prim算法 初始时所有点都是蓝点,min[1]=0,min[2、3、4、5]=∞。权值之和MST=0。 1 2 3 4 5 7 6 第一次循环自然是找到min[1]=0最小的蓝点1。将1变为白点,接着枚举与1相连的所有蓝点2、3、4,修改它们与白点相连的最小边权。 1 2 3 4 5 7 6 min[2]=w[1][2]=2; min[3]=w[1][3]=4; min[4]=w[1][4]=7;
Prim算法 第二次循环是找到min[2]最小的蓝点2。将2变为白点,接着枚举与2相连的所有蓝点3、5,修改它们与白点相连的最小边权。 1 2 3 4 5 7 6 min[3]=w[2][3]=1; min[5]=w[2][5]=2; 第三次循环是找到min[3]最小的蓝点3。将3变为白点,接着枚举与3相连的所有蓝点4、5,修改它们与白点相连的最小边权。 1 2 3 4 5 7 6 min[4]=w[3][4]=1; 由于min[5]=2 < w[3][5]=6;所以不修改min[5]的值。
Prim算法 2 5 1 6 3 7 4 最后两轮循环将点4、5以及边w[2][5],w[3][4]添加进最小生成树。 最后权值之和MST=6。 这n次循环,每次循环我们都能让一个新的点加入生成树,n次循环就能把所有点囊括到其中;每次循环我们都能让一条新的边加入生成树,n-1次循环就能生成一棵含有n个点的树;每次循环我们都取一条最小的边加入生成树,n-1次循环结束后,我们得到的就是一棵最小的生成树。 这就是Prim采取贪心法生成一棵最小生成树的原理。 算法时间复杂度:O (N2)。
Prim算法 【例4-10】、最优布线问题(wire.cpp) 【问题描述】 学校有n台计算机,为了方便数据传输,现要将它们用数据线连接起来。两台计算机被连接是指它们间有数据线连接。由于计算机所处的位置不同,因此不同的两台计算机的连接费用往往是不同的。 当然,如果将任意两台计算机都用数据线连接,费用将是相当庞大的。为了节省费用,我们采用数据的间接传输手段,即一台计算机可以间接的通过若干台计算机(作为中转)来实现与另一台计算机的连接。 现在由你负责连接这些计算机,任务是使任意两台计算机都连通(不管是直接的或间接的)。 【输入格式】 输入文件wire.in,第一行为整数n(2<=n<=100),表示计算机的数目。此后的n行,每行n个整数。第x+1行y列的整数表示直接连接第x台计算机和第y台计算机的费用。 【输出格式】 输出文件wire.out,一个整数,表示最小的连接费用。 【输入样例】 3 0 1 2 1 0 1 2 1 0 【输出样例】 2 (注:表示连接1和2,2和3,费用为2)
Prim算法 【参考程序】 //wire #include<iostream> #include<cstdio> #include<cstring> using namespace std; int g[101][101]; //邻接矩阵 int minn[101]; //minn[i]存放蓝点i与白点相连的最小边权 bool u[101]; //u[i]=True,表示顶点i还未加入到生成树中 //u[i]=False,表示顶点i已加入到生成树中 int n,i,j; int main() { freopen("wire.in","r",stdin); freopen("wire.out","w",stdout); cin >> n; for (i = 1; i <= n; i++) for (j = 1; j <= n; j++) cin >> g[i][j]; memset(minn,0x7f,sizeof(minn)); //初始化为maxint minn[1] = 0; memset(u,1,sizeof(u)); //初始化为True,表示所有顶点为蓝点
Prim算法 for (i = 1; i <= n; i++) { int k = 0; for (j = 1; j <= n; j++) //找一个与白点相连的权值最小的蓝点k if (u[j] && (minn[j] < minn[k])) k = j; u[k] = false; //蓝点k加入生成树,标记为白点 for (j = 1; j <= n; j++) //修改与k相连的所有蓝点 if (u[j] && (g[k][j] < minn[j])) minn[j] = g[k][j]; } int total = 0; for (i = 1; i <= n; i++) //累加权值 total += minn[i]; cout << total << endl; return 0;
Kruskal算法 1 2 3 4 5 6 Kruskal(克鲁斯卡尔)算法是一种巧妙利用并查集来求最小生成树的算法。 首先我们把无向图中相互连通的一些点称为处于同一个连通块中。例如下图 图中有3个连通块。1、2处于一个连通块中,4、5、6也处于一个连通块中,孤立点3也称为一个连通块。 Kruskal算法将一个连通块当做一个集合。Kruskal首先将所有的边按从小到大顺序排序(一般使用快排),并认为每一个点都是孤立的,分属于n个独立的集合。然后按顺序枚举每一条边。如果这条边连接着两个不同的集合,那么就把这条边加入最小生成树,这两个不同的集合就合并成了一个集合;如果这条边连接的两个点属于同一集合,就跳过。直到选取了n-1条边为止。 1 2 3 4 5 6
Kruskal算法 算法描述: 初始化并查集。father[x]=x。 tot=0 将所有边用快排从小到大排序。 计数器 k=0; for (i=1; i<=M; i++) //循环所有已从小到大排序的边 if 这是一条u,v不属于同一集合的边(u,v)(因为已经排序,所以必为最小), begin ①合并u,v所在的集合,相当于把边(u,v)加入最小生成树。 ②tot=tot+W(u,v) ③k++ ④如果k=n-1,说明最小生成树已经生成,则break; end; 6. 结束,tot即为最小生成树的总权值之和。
Kruskal算法 思想讲解: Kruskal(克鲁斯卡尔)算法开始时,认为每一个点都是孤立的,分属于n个独立的集合。 1 2 3 4 5 12 8 9 6 7 10 5个集合{ {1},{2},{3},{4},{5} } 生成树中没有边 Kruskal每次都选择一条最小的边,而且这条边的两个顶点分属于两个不同的集合。将选取的这条边加入最小生成树,并且合并集合。
Kruskal算法 第一次选择的是<1,2>这条边,将这条边加入到生成树中,并且将它的两个顶点1、2合并成一个集合。 1 2 3 4 5 12 8 9 6 7 10 4个集合{ {1,2},{3},{4},{5} } 生成树中有一条边{ <1,2> } 第二次选择的是<4,5>这条边,将这条边加入到生成树中,并且将它的两个顶点4、5合并成一个集合。 1 2 3 4 5 12 8 9 6 7 10 3个集合{ {1,2},{3},{4,5} } 生成树中有2条边{ <1,2> ,<4,5>}
Kruskal算法 第三次选择的是<3,5>这条边,将这条边加入到生成树中,并且将它的两个顶点3、5所在的两个集合合并成一个集合 1 2 3 4 5 12 8 9 6 7 10 2个集合{ {1,2},{3,4,5} } 生成树中有3条边{ <1,2> ,<4,5>,<3,5>} 第四次选择的是<2,5>这条边,将这条边加入到生成树中,并且将它的两个顶点2、5所在的两个集合合并成一个集合。 1 2 3 4 5 12 8 9 6 7 10 1个集合{ {1,2,3,4,5} } 生成树中有4条边{ <1,2> ,<4,5>,<3,5>,<2,5>}
Kruskal算法 算法结束,最小生成树权值为19。 通过上面的模拟能够看到,Kruskal算法每次都选择一条最小的,且能合并两个不同集合的边,一张n个点的图总共选取n-1次边。因为每次我们选的都是最小的边,所以最后的生成树一定是最小生成树。每次我们选的边都能够合并两个集合,最后n个点一定会合并成一个集合。通过这样的贪心策略,Kruskal算法就能得到一棵有n-1条边,连接着n个点的最小生成树。 Kruskal算法的时间复杂度为O(E*logE),E为边数。
Kruskal算法 【例4-11】、最短网络Agri-Net 【问题描述】 农民约翰被选为他们镇的镇长!他其中一个竞选承诺就是在镇上建立起互联网,并连接到所有的农场。当然,他需要你的帮助。约翰已经给他的农场安排了一条高速的网络线路,他想把这条线路共享给其他农场。为了用最小的消费,他想铺设最短的光纤去连接所有的农场。你将得到一份各农场之间连接费用的列表,你必须找出能连接所有农场并所用光纤最短的方案。每两个农场间的距离不会超过100000。 【输入格式】 第一行: 农场的个数,N(3<=N<=100)。 第二行..结尾 后来的行包含了一个N*N的矩阵,表示每个农场之间的距离。理论上,他们是N行,每行由N个用空格分隔的数组成,实际上,他们限制在80个字符,因此,某些行会紧接着另一些行。当然,对角线将会是0,因为不会有线路从第i个农场到它本身。 【输出格式】 只有一个输出,其中包含连接到每个农场的光纤的最小长度。 【输入样例】agrinet.in 4 0 4 9 21 4 0 8 17 9 8 0 16 21 17 16 0 【输出样例】agrinet.out 28
Kruskal算法 【参考程序】 //agrinet #include<cstdio> #include<iostream> #include<algorithm> //sort()需要用到<algorithm>库 using namespace std; struct point { int x; int y; int v; }; //定义结构类型,表示边 point a[9901]; //存边 int fat[101]; int n,i,j,x,m,tot,k; int father(int x) if (fat[x] != x) fat[x] = father(fat[x]); return fat[x]; } void unionn(int x,int y) int fa = father(x); int fb = father(y); if (fa != fb) fat[fa] = fb;
Kruskal算法 int cmp(const point &a,const point &b) //sort()自定义的比较函数 { if (a.v < b.v) return 1; else return 0; } int main() freopen("agrinet.in","r",stdin); freopen("agrinet.out","w",stdout); cin >> n; for (i = 1; i <= n; i++) for (j = 1; j <= n; j++) cin >> x; if (x != 0) m++; a[m].x = i; a[m].y = j; a[m].v = x; for (i = 1; i <= n; i++) fat[i] = i; sort(a+1,a+m+1,cmp); //C++标准库中自带的快排 //cmp为自定义的比较函数。表示a数组的1-m按规则cmp排序
Kruskal算法 for (i = 1; i <= m; i++) { if (father(a[i].x) != father(a[i].y)) unionn(a[i].x,a[i].y); tot += a[i].v; k++; } if (k == n-1) break; cout << tot; return 0;
上机练习 1、局域网 【问题描述】 某个局域网内有n(n<=100)台计算机,由于搭建局域网时工作人员的疏忽,现在局域网内的连接形成了回路,我们知道如果局域网形成回路那么数据将不停的在回路内传输,造成网络卡的现象。因为连接计算机的网线本身不同,所以有一些连线不是很畅通,我们用f(i,j)表示i,j之间连接的畅通程度(f(i,j)<=1000),f(i,j)值越小表示i,j之间连接越通畅,f(i,j)为0表示i,j之间无网线连接。现在我们需要解决回路问题,我们将除去一些连线,使得网络中没有回路,并且被除去网线的Σf(i,j)最大,请求出这个最大值。 【输入格式】 第一行两个正整数n k 接下来的k行每行三个正整数i j m表示i,j两台计算机之间有网线联通,通畅程度为m。 【输出格式】 一个正整数,Σf(i,j)的最大值 【输入输出样例】 【输入样例】 【输出样例】 5 5 1 2 8 1 3 1 1 5 3 2 4 5 3 4 2 8
上机练习 2、繁忙的都市 【问题描述】 城市C是一个非常繁忙的大都市,城市中的道路十分的拥挤,于是市长决定对其中的道路进行改造。城市C的道路是这样分布的:城市中有n个交叉路口,有些交叉路口之间有道路相连,两个交叉路口之间最多有一条道路相连接。这些道路是双向的,且把所有的交叉路口直接或间接的连接起来了。每条道路都有一个分值,分值越小表示这个道路越繁忙,越需要进行改造。但是市政府的资金有限,市长希望进行改造的道路越少越好,于是他提出下面的要求: 改造的那些道路能够把所有的交叉路口直接或间接的连通起来。 在满足要求1的情况下,改造的道路尽量少。 在满足要求1、2的情况下,改造的那些道路中分值最大值尽量小。 【编程任务】 作为市规划局的你,应当作出最佳的决策,选择那些道路应当被修建。 【输入格式】city.in 第一行有两个整数n,m表示城市有n个交叉路口,m条道路。接下来m行是对每条道路的描述,u, v, c表示交叉路口u和v之间有道路相连,分值为c。(1≤n≤300,1≤c≤10000) 【输出格式】city.out 两个整数s, max,表示你选出了几条道路,分值最大的那条道路的分值是多少。 【输入输出样例】 【输入样例】 【输出样例】 4 5 1 2 3 1 4 5 2 4 7 2 3 6 3 4 8 3 6