最小生成树
什么是最小生成树
- 给定一张无向带权图G=(V,E,W),从中挑选出|V|-1条边,使得V中任意两点都有直接或者间接的路径
- 显然,最后得到的子图是一棵树,如果这棵树是所有树中边权和最小的,那么它就是图G的最小生成树
- 一幅图中的最小生成树可能不止一个
克鲁斯卡尔算法
-
算法流程
- 把边按照从小到大排序等待挑选进集合S
- 尝试加入当前给定的边,如果加入这个边使得集合S中不存在环,则加入该边,否则跳过
- 当遍历完所有边或者S集合中已经包含所有顶点时算法结束
-
源码 HDU1223
#include<iostream>
#include<queue>
#include<list>
#include<vector>
#include<cstring>
#include<set>
#include<stack>
#include<map>
#include<cmath>
#include<algorithm>
#include<string>
#include<stdio.h>
using namespace std;
typedef long long ll;
#define MS(x,i) memset(x,i,sizeof(x))
#define rep(i,s,e) for(int i=s; i<=e; i++)
#define sc(a) scanf("%d",&a)
#define scl(a) scanf("%lld",&a)
#define sc2(a,b) scanf("%d %d", &a, &b)
#define debug printf("debug......
");
#define pfd(x) printf("%d
",x)
#define pfl(x) printf("%lld
",x)
const double eps=1e-8;
const double PI = acos(-1.0);
const int inf = 0x3f3f3f3f;
const ll INF = 0x7fffffff;
const int maxn = 2e2+10;
const int M = 1e4+10;
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0 , 0};
int n;//边数
int m;//顶点数
int fa[maxn];//根节点
//把边按照从小到大排序
struct node{
int u,v,w;
}edge[M];
bool cmp(node a, node b){
return a.w < b.w;
}
//并查集基本操作
void init(){
rep(i,1,n){
fa[i] = i;
}
}
int find(int x){
if(x != fa[x]){
return fa[x] = find(fa[x]);
}
return fa[x];
}
void merge(int x, int y){
int fx = find(x);
int fy = find(y);
if(fx != fy){
fa[fx] = fa[fy];
}
}
int main(){
int x,y,w;
ios::sync_with_stdio(false);
while(cin>>n){
if(n == 0) break;
m = (n-1)*n/2;
int sum = 0;
init();
rep(i , 1, m){
cin>>x>>y>>w;
edge[i].u = x;
edge[i].v = y;
edge[i].w = w;
}
sort(edge+1, edge+1+m,cmp);//把边从小到大
//从小到大筛边 遇到 环则跳过
rep(i,1,m){
node cur = edge[i];
int u = cur.u;
int v = cur.v;
int w = cur.w;
//如果u,v处于同一个集合,在加入uv边则必形成环
if(find(u) != find(v)){
sum += w;
merge(u,v);
}
}
cout<<sum<<endl;
}
return 0;
}
Prim算法
-
算法流程
- 从任意源点s出发,s加入集合S
- 对于S集合中的所有点,找出这些点所连接的所有边,选出一个最短边uv,其中u属于集合S,v不属于集合S,把v加进集合S
- 当集合S中包含所有顶点,算法结束
-
源码 HDU1223
#include<iostream>
#include<queue>
#include<list>
#include<vector>
#include<cstring>
#include<set>
#include<stack>
#include<map>
#include<cmath>
#include<algorithm>
#include<string>
#include<stdio.h>
using namespace std;
typedef long long ll;
#define MS(x,i) memset(x,i,sizeof(x))
#define rep(i,s,e) for(int i=s; i<=e; i++)
#define sc(a) scanf("%d",&a)
#define scl(a) scanf("%lld",&a)
#define sc2(a,b) scanf("%d %d", &a, &b)
#define debug printf("debug......
");
#define pfd(x) printf("%d
",x)
#define pfl(x) printf("%lld
",x)
const double eps=1e-8;
const double PI = acos(-1.0);
const int inf = 0x3f3f3f3f;
const ll INF = 0x7fffffff;
const int maxn = 2e2+10;
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0 , 0};
int dis[maxn];//记录每个顶点到S集合的所有边的长度
int vis[maxn];//点是否在S集合
int G[maxn][maxn];//存图
int x,y,w;
int sum;
int n,m;
void Prim(int s){
MS(vis , 0);
vis[s] = 1;
//s进入S集合,初始化一下dis
rep(i,1,n){
dis[i] = G[i][s];
}
//还需要选n-1个点
rep(i , 1 , n-1){
int minx=inf;
int index;
//从dis中找出一个最小的
rep(j,1,n){
if(!vis[j] && dis[j] < minx){
minx = dis[j];
index = j;
}
}
//这个顶点被加入S集合 因为这个顶点到S集合的边最小
vis[index] = 1;
sum += minx;//最小生成树总长加上该边的值
//由于这个顶点的加入,使得余下V-S顶点到S集合的距离发生了变化
rep(j , 1, n){
if(!vis[j] && G[index][j] < dis[j]){
dis[j] = G[index][j];
}
}
}
}
int main(){
//while(cin>>n>>m){
while(cin>>n){
if(n == 0) break;
rep(i,1,n)
rep(j,1,n)
if(i==j) G[i][j] = 0;
else G[i][j] = inf;
sum = 0;
rep(i,1,n*(n-1)/2){
cin>>x>>y>>w;
G[x][y] = w;
G[y][x] = w;
}
Prim(1);
cout<<sum<<endl;
}
return 0;
}
算法正确性证明
-
Prim算法 为什么每次选取最短边的策略是正确的?
- 用归纳法证明,首先如果只有一个顶点,显然成立 n=1
- 假设n=k成立,即S集合中已经有k个顶点,并且选择的边都是最小生成树的边,那么当n=k+1时,如果选择的边e'不是最短边e,则最后形成的生成树中,我们可以加入e,则必然形成一个环,我们把e'去掉则会得到另外一棵生成树,显然这个生成树的边权和更小。即n=k+1也是成立的。
-
Kruscal算法
- 从Prim算法我们可以得出结论:“如果某个连通图属于最小生成树,那么所有从外部连接到该连通图的边中的一条最短的边必然属于最小生成树”
- 当最小生成树被拆分成彼此独立的若干个连通分量的时候,所有能够连接任意两个连通分量的边中的一条最短边必然属于最小生成树
堆优化的prim算法
- prim算法核心
void Prim(int s){
MS(vis , 0);
vis[s] = 1;
//s进入S集合,初始化一下dis
rep(i,1,n){
dis[i] = G[i][s];
}
//还需要选n-1个点
rep(i , 1 , n-1){
int minx=inf;
int index;
//*********优化点A***********
//从dis中找出一个最小的
rep(j,1,n){
if(!vis[j] && dis[j] < minx){
minx = dis[j];
index = j;
}
}
//这个顶点被加入S集合 因为这个顶点到S集合的边最小
vis[index] = 1;
sum += minx;//最小生成树总长加上该边的值
//*********优化点B***********
//由于这个顶点的加入,使得余下V-S顶点到S集合的距离发生了变化
rep(j , 1, n){
if(!vis[j] && G[index][j] < dis[j]){
dis[j] = G[index][j];
}
}
}
}
-
优化点A
- 每次从1到n扫描显然比较慢,可以把待选边加入一个优先队列,每次找最小的边的时候就取出队首
-
优化点B
- 很显然不需要更新所有的点到S集合的距离,只需要对新加入 S集合的点所引出的到达点进行距离更新即可(也就是把这些边加入优先队列)
-
源码 HDU1223 洛谷P3366
#include<iostream>
#include<queue>
#include<list>
#include<vector>
#include<cstring>
#include<set>
#include<stack>
#include<map>
#include<cmath>
#include<algorithm>
#include<string>
#include<stdio.h>
using namespace std;
typedef long long ll;
#define MS(x,i) memset(x,i,sizeof(x))
#define rep(i,s,e) for(int i=s; i<=e; i++)
#define sc(a) scanf("%d",&a)
#define scl(a) scanf("%lld",&a)
#define sc2(a,b) scanf("%d %d", &a, &b)
#define debug printf("debug......
");
#define pfd(x) printf("%d
",x)
#define pfl(x) printf("%lld
",x)
const double eps=1e-8;
const double PI = acos(-1.0);
const int inf = 0x3f3f3f3f;
const ll INF = 0x7fffffff;
const int maxn = 2e2+10;
const int M = 1e4+10;
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0 , 0};
int n,m;
int cnt;
int head[maxn];
struct node{
int v;
int w;
int nxt;
node(){}
node(int a, int b){v=a;w=b;}
}edge[2*M];//存图
bool operator <(node a, node b){
return a.w > b.w;
}
void addEdge(int u, int v, int w){
edge[cnt].v = v;
edge[cnt].w = w;
edge[cnt].nxt = head[u];
head[u] = cnt++;
}
int sum;
bool vis[maxn];//表示i是否被加进集合S
//队列元素
struct ed{
int u,v,w;
};
bool in[2*M];//边i是否在队列中
void prim(int s){
sum = 0;
priority_queue<node> q;
MS(vis , 0);
MS(in , 0);
vis[s] = 1;
for(int i=head[s]; i; i=edge[i].nxt){
int v = edge[i].v;
int w = edge[i].w;
q.push(node(v , w));
in[i] = 1;
}
while(!q.empty()){
node tp = q.top();
int v = tp.v;
int w = tp.w;
q.pop();
if(vis[v]){
cout<<"顶点 "<<v<<" 已经在集合S中"<<endl;
continue;
}
vis[v] = 1;//顶点v被取出
cout<<"顶点 "<<v<<" 加入集合S中"<<endl;
sum += w;
for(int i=head[v]; i; i=edge[i].nxt){
int to = edge[i].v;
int tw = edge[i].w;
//如果这个顶点已经在S集合 或者 这条边已经在队列中就不加了
if(!vis[to] && !in[i])
q.push(node(to , tw));
}
}
}
int main(){
while(sc(n)!=EOF){
m = (n-1)*n/2;
int u,v,w;
MS(head , 0);
rep(i,1,m){
sc(u);
sc(v);
sc(w);
addEdge(u , v, w);
addEdge(v, u, w);
}
prim(1);
/*rep(i,1,n)
if(!vis[i]){
printf("orz
");
continue;
}*/
pfd(sum);
}
return 0;
}