什么是生成树?对连通图进行遍历,过程中经过的点和边的组合可看成一棵树,也叫生成树
最小生成树引入
世界上有着许许多多的铁路线、公路线,想要从一个城市到另一个城市修一条线路需要许多资金,当然,修路的方式有多种多样,现在我们想知道如何修路能使得这些城市之间形成一个通信网,并且使得总耗费最少呢?
- 假设有n个城市,那么我们最少需要修n-1条路,这是明显的,把这些路连在一起,加上城市节点,就构成了一棵生成树,在这些生成树中,边权之和最小的就是最小生成树,所以我们可以看出最小生成树不一定是唯一的
- 最小生成树(Minimum Spanning Tree)简称MST
- 下面两种算法都是基于贪心的思想
Prim算法
如何构建最小生成树呢?我们需要三个数组:selected,minDist,parent
- selected数组的作用是判断节点是否已被选
- minDist数组的作用是记录当前可供选择的边权
- parent数组的作用是记录这些边的上一个节点
- 将所有顶点分成两个集合U和V-U,集合U是最小生成树的节点集,最开始为空,集合V-U是尚未被加入到树中的节点,最开始为所有节点
- 我们从任意一个节点出发,首先把它加入到集合U中,同时用它的所有边去更新minDist数组,使得数组中边总是最小
- 接着我们搜索一遍minDist中的边,找到最小的那一个,把它所对的节点(不是parent)加入到最小生成树中,同时selected数组记录
prim算法步骤主要有三步:Update,Scan,Add 也就是更新、搜索、加点,所以prim算法又叫做加点法
习题
- 有两个WA点,第一个点是图是无向图,第二个是可能出现两个点之间有两条边或者更多,这个时候我们要取最短的边,这样需要在输入那里处理一下
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <iomanip>
#include <queue>
#include <stack>
using namespace std;
typedef long long ll;
const int MAXN = 5050;
int edge[MAXN][MAXN];
int selected[MAXN];
int minDist[MAXN];
int parent[MAXN];
int ans = 0;
int tot = 1;
void Prim(int num,int m){
selected[m] = 1;
int k,tmp;
for(int i=1;i<=num;i++){
if(edge[m][i]){
minDist[i] = edge[m][i];
parent[i] = m;
}
}
for(int i=1;i<num;i++){
tmp = 0x3f3f3f3f;
for(int j=1;j<=num;j++){
if(!selected[j]&&minDist[j]<tmp){
k = j;
tmp = minDist[j];
}
}
if(selected[k]) continue;
selected[k] = 1;
tot++;
ans += edge[parent[k]][k];
for(int j=1;j<=num;j++){
if(!selected[j]&&edge[k][j]<minDist[j]&&edge[k][j]){
minDist[j] = edge[k][j];
parent[j] = k;
}
}
}
if(tot!=num) cout<<"orz"<<endl;
else cout<<ans<<endl;
}
int main(){
int n,m;
cin>>n>>m;
for(int i=0;i<m;i++){
int x,y,z;
cin>>x>>y>>z;
if(edge[x][y]==0){
edge[x][y] = z;
edge[y][x] = z;
}
else if(z<edge[x][y]){
edge[x][y] = z;
edge[y][x] = z;
}
}
memset(parent,-1,sizeof parent);
memset(minDist,0x3f,sizeof minDist);
Prim(n,1);
return 0;
}
HDU模板题
这道题坑死我了,一直看了两个小时没找到错误,一直提示非法访问内存空间,数组大小也很合适,后来换了C++提交就过了,很奇怪
- 和上一道题基本一样,唯一需要注意的是使用的变量和数组需要恢复原状
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <iomanip>
#include <queue>
#include <stack>
using namespace std;
typedef long long ll;
const int MAXN = 505;
int edge[MAXN][MAXN];
int selected[MAXN];
int minDist[MAXN];
int parent[MAXN];
int ans = 0;
int tot = 1;
void Prim(int num,int m){
tot = 1;
ans = 0;
selected[m] = 1;
int k,tmp;
for(int i=1;i<=num;i++){
if(edge[m][i]){
minDist[i] = edge[m][i];
parent[i] = m;
}
}
for(int i=1;i<num;i++){
tmp = 0x3f3f3f3f;
for(int j=1;j<=num;j++){
if(!selected[j]&&minDist[j]<tmp){
k = j;
tmp = minDist[j];
}
}
if(selected[k]) break;
selected[k] = 1;
tot++;
ans += edge[parent[k]][k];
for(int j=1;j<=num;j++){
if(!selected[j]&&edge[k][j]<minDist[j]&&edge[k][j]){
minDist[j] = edge[k][j];
parent[j] = k;
}
}
}
if(tot!=num) cout<<"?"<<endl;
else cout<<ans<<endl;
}
int main(){
int n,m;
while(cin>>n>>m){
if(n==0) break;
memset(edge,0,sizeof edge);
for(int i=0;i<n;i++){
int x,y,z;
cin>>x>>y>>z;
if(edge[x][y]==0){
edge[x][y] = z;
edge[y][x] = z;
}
else if(z<edge[x][y]){
edge[x][y] = z;
edge[y][x] = z;
}
}
memset(parent,-1,sizeof parent);
memset(minDist,0x3f,sizeof minDist);
memset(selected,0,sizeof selected);
Prim(m,1);
}
return 0;
}
更新一道题
- 此题依然是模板,但这个问题是边多,内存又有限制,不适合使用方便的kruskal,需要用prim,可以借助这道题再次熟悉prim
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <iomanip>
#include <queue>
#include <stack>
using namespace std;
typedef long long ll;
const int MAXN = 5050;
int selected[MAXN];
double minDist[MAXN];
int parent[MAXN];
double ans = 0;
int tot = 1;
struct node{
int x, y;
}st[MAXN];
double DIS(node x, node y){
double dx = x.x - y.x;
double dy = x.y - y.y;
return sqrt(dx * dx + dy * dy);
}
void Prim(int num,int m){
selected[m] = 1;
int k;
double tmp;
for(int i=1;i<num;i++){
double n = DIS(st[m], st[i]);
if(n > 0){
minDist[i] = n;
parent[i] = m;
}
}
for(int i=1;i<num;i++){
tmp = 9999999999;
for(int j=1;j<num;j++){
if(!selected[j]&&minDist[j]<tmp){
k = j;
tmp = minDist[j];
}
}
if(selected[k]) continue;
selected[k] = 1;
tot++;
ans += DIS(st[parent[k]], st[k]);
for(int j=1;j<=num;j++){
double n = DIS(st[k], st[j]);
if(!selected[j] && n<minDist[j]){
minDist[j] = n;
parent[j] = k;
}
}
}
printf("%.2lf", ans);
}
int main(){
int n;
cin>>n;
for(int i=0;i<n;i++) cin>>st[i].x>>st[i].y;
memset(parent,-1,sizeof parent);
memset(minDist,0x3f,sizeof minDist);
Prim(n,0);
return 0;
}
Kruskal算法
相对于Prim算法,Kruskal算法的思想是加边,可以称之为加边法,过程如下
- 把所有边按照权值升序排列,从前往后依次‘回贴’边到图中,每次需要判断图中是否出现环,如果出现就舍弃转向下一条边,这样进行下去,直到所有点都在生成树里,这样就形成了最小生成树
- 不知道有没有人会有这样的疑问,为什么Kruskal算法能够找到最小生成树,Prim是从顶点出发,向外扩散,肯定能遍历全图,这不难理解;但Kruskal是不停地加边,中间还有舍弃,难道不会漏边吗?
- 仔细思考可以明白,我们已经将边排好序了,如果后面的边使得前面的边构成的图出现环,可以直接把这条边舍弃掉,因为前面的边已经使现在的顶点连在一片,而这条边只不过是多余的而已
- 思路已经有了,那么具体如何实现呢?
这里要用到一点并查集的相关知识,简单来讲,刚开始,不同的顶点属于不同的集合,每次选择一条边加进来都要把这两个点划分到一个集合里面去,这就是典型的并查集,在前面路径压缩优化一下,也就是把所有点直接跟着根节点而不是递归查找 - 相对于Prim的实现,这一次使用Kruskal显得顺利得多,没有发现什么坑点
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <iomanip>
#include <queue>
#include <stack>
using namespace std;
typedef long long ll;
const int MAXN = 2e5+100;
struct Edge{
int x,y,z;}edge[MAXN];
int set[MAXN];
bool cmp(Edge x,Edge y){
return x.z<y.z;
}
int Findset(int x){
if(x!=set[x]) set[x] = Findset(set[x]);
return set[x];
}
void Kruskal(int num,int m){
int ans = 0;
int tot = 0;
for(int i=1;i<=num;i++) set[i] = i;
for(int i=0;i<m;i++){
int x = Findset(edge[i].x);
int y = Findset(edge[i].y);
if(x == y) continue;
set[x] = set[y];
tot++;
ans += edge[i].z;
if(tot == num-1) break;
}
if(tot == num-1) cout<<ans<<endl;
else cout<<"orz"<<endl;
}
int main(){
int n,m;
cin>>n>>m;
for(int i=0;i<m;i++) cin>>edge[i].x>>edge[i].y>>edge[i].z;
sort(edge,edge+m,cmp);
Kruskal(n,m);
return 0;
}
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include <cmath>
#include <iomanip>
#include <queue>
#include <stack>
using namespace std;
typedef long long ll;
const int MAXN = 2e5+100;
struct Edge{
int x,y,z;}edge[MAXN];
int set[MAXN];
bool cmp(Edge x,Edge y){
return x.z<y.z;
}
int Findset(int x){
if(x!=set[x]) set[x] = Findset(set[x]);
return set[x];
}
void Kruskal(int num,int m){
int ans = 0;
int tot = 0;
for(int i=1;i<=num;i++) set[i] = i;
for(int i=0;i<m;i++){
int x = Findset(edge[i].x);
int y = Findset(edge[i].y);
if(x == y) continue;
set[x] = set[y];
tot++;
ans += edge[i].z;
if(tot == num-1) break;
}
if(tot == num-1) cout<<ans<<endl;
else cout<<"?"<<endl;
}
int main(){
int n,m;
while(cin>>n>>m){
if(n==0) break;
for(int i=0;i<n;i++) cin>>edge[i].x>>edge[i].y>>edge[i].z;
sort(edge,edge+n,cmp);
Kruskal(m,n);
}
return 0;
}