在学习最小生成树算法之前我们先了解一些基本概念
生成树与最小生成树
生成树是一个连通图的连通子图,包含图中的n个点,n-1条边。最小生成树是这n-1条边的权值之和最小的一颗生成树。
Prim算法
Prim算法可在加权连通图里搜索最小生成树,通俗来说就是在一个带权值的连通图中保留n-1条边(这n-1条边的权值和最小),但依旧使这个图为连通图。Prim算法是基于贪心的思想,每次都选择一条符合条件的权值最小的边对于的点加入生成树。
Prim算法的时间复杂度为O(N^2),适合稠密图
通常最小生成树问题一般用邻接矩阵存图,比较方便
流程为:
首先初始化数组dis【】、pre【】,dis【i】表示点 i 在生成树中需要耗费的代价,dis【1】 = 0;pre【i】代表生成树中pre【i】与点 i 相连(前驱),pre【1】 = -1,代表没有前驱,vis【i】 = 1表示以访问。
id | 1 | 2 | 3 | 4 | 5 | 6 |
vis | 1 | 0 | 0 | 0 | 0 | 0 |
dis | 0 | 6 | 1 | 5 | inf | inf |
pre | - 1 | 1 | 1 | 1 | 1 | 1 |
之后从上表中选一个dis【】距离最小,且vis【】 = 0 的点,然后用这个点去更新他邻接点(未访问)在最小生成树中的距离,且更新前驱,这里的点为3
更新上表得
id | 1 | 2 | 3 | 4 | 5 | 6 |
vis | 1 | 0 | 1 | 0 | 0 | 0 |
dis | 0 | 5 | 1 | 5 | 6 | 4 |
pre | - 1 | 3 | 1 | 1 | 3 | 3 |
选择点6,更新其他点
id | 1 | 2 | 3 | 4 | 5 | 6 |
vis | 1 | 0 | 1 | 0 | 0 | 1 |
dis | 0 | 5 | 1 | 2 | 6 | 4 |
pre | - 1 | 3 | 1 | 6 | 3 | 3 |
接下来符合条件的点为4,更新其他数组
id | 1 | 2 | 3 | 4 | 5 | 6 |
vis | 1 | 0 | 1 | 1 | 0 | 1 |
dis | 0 | 5 | 1 | 2 | 6 | 4 |
pre | - 1 | 3 | 1 | 6 | 3 | 3 |
接下来选择点2,更新数据
id | 1 | 2 | 3 | 4 | 5 | 6 |
vis | 1 | 1 | 1 | 1 | 0 | 1 |
dis | 0 | 5 | 1 | 2 | 3 | 4 |
pre | - 1 | 3 | 1 | 6 | 2 | 3 |
接下来是点5,已更新完所有数据,dis【】数组中卫最小生成树的权值,pre【】数组中记录的是最小生成树的边
例题:HDU1102
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> PI;
const int inf = 0x3f7f7f7f;
const int N = 105;
int mp[N][N];
int lowcost[N];
int pre[N];
int vis[N];
int n;
int prim(int s)
{
int ans = 0;
memset(vis,0,sizeof(vis));
for(int i = 1; i <= n; i ++){
lowcost[i] = mp[s][i];
pre[i] = s;
}
vis[s] = 1;
pre[s] = -1;
for(int i = 1; i < n; i ++)
{
int pos = -1;
int mmin = inf;
for(int j = 1; j <= n; j ++){
if(vis[j]==0 && lowcost[j] < mmin){
pos = j;
mmin = lowcost[j];
}
}
if(pos == -1) return -1;
vis[pos] = 1;
ans += mmin;
for(int j = 1; j <= n; j ++){
if(lowcost[j] > mp[pos][j] && vis[j] ==0){
lowcost[j] = mp[pos][j];
pre[j] = pos;
}
}
}
return ans;
}
int main()
{
while(~scanf("%d",&n))
{
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j ++)
scanf("%d",&mp[i][j]);
int q;
scanf("%d",&q);
for(int i = 0; i < q; i ++){
int u,v;
scanf("%d%d",&u,&v);
mp[u][v] = mp[v][u] = 0;
}
printf("%d\n",prim(1));
/*
for(int i = 2; i <= n; i ++)
{
printf("%d-->%d\n",i,pre[i]);
}
}
*/
return 0;
}
//HDU1102
Kruskal算法
在学习这个算法之前,我们需要先学习一种数据结构:并查集
并查集:.是一种关于集合的操作,能够快速的找到某个元素所在的集合,而且还能够实现集合之间的合并。
并查集包含三种操作:
1、初始化操作:最开始将每个元素出事化为一个集合。pre【i】代表着i点所在的集合,最开始pre【i】 = i;
2、查找元素所在的集合:也就是查找这个集合所有元素的祖先
如pre【a】 = b,pre【b】 = c,pre【c】 = d,pre【d】 = d,这里d就是所有元素的祖先
3、合并操作,合并两个不同的集合,也就是找到这两个集合的祖先,使其中一个祖先指向另一个集合。
并查集的优化:
这个图中我们可以看出要找到点a的祖先,需要经过好几次循环,那么我就可以让这个集合中的所有元素都直接指向祖先,这样就可以在下次查找时O(1)的复杂度就能够找到。
关键代码:
int findx(int x)
{
int px = x;
while(pre[x] != x)
x = pre[x];
while(px != x)
{
int pr = pre[px];
pre[px] = x;
px = pr;
}
return x;
}
void link(int u, int v)
{
pre[u] = v;
}
Kruskal算法
kruskal算法是基于贪心的思想,利用并查集实现的,每次选着一条权值最小的符合条件的边(条件:这条边的两个点不再同一个集合)将其加入生成树集合
过程:
1、将边按权值大小排序
2、选择一条权值最小的边,且这条边的两点属于不同的集合,然后合并这两个集合
3、重复以上操作直到所有点都属于一个集合
例题:HDU1102
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> PI;
const int inf = 0x3f7f7f7f;
const int N = 200;
int mp[N][N];
int pre[N];
int n;
struct edge
{
int u,v,w;
}grap[N*N];
bool cmp(edge a,edge b)
{
return a.w < b.w;
}
int findx(int x)
{
int px = x;
while(pre[x] != x)
x = pre[x];
while(px != x)
{
int pr = pre[px];
pre[px] = x;
px = pr;
}
return x;
}
void link(int u, int v)
{
pre[u] = v;
}
int main()
{
while(~scanf("%d",&n))
{
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= n; j ++)
scanf("%d",&mp[i][j]);
int q;
scanf("%d",&q);
for(int i = 0; i < q; i ++)
{
int u,v;
scanf("%d%d",&u,&v);
mp[u][v] = mp[v][u] = 0;
}
int cnt = 0;
for(int i = 1; i<= n*n; i ++)
{
for(int j = i+1; j <= n; j ++)
{
grap[cnt].u = i;
grap[cnt].v = j;
grap[cnt++].w = mp[i][j];
}
}
for(int i = 1; i <= n; i ++)
pre[i] = i;
sort(grap,grap+cnt,cmp);
int ans = 0;
for(int i = 0; i < cnt; i ++)
{
int u = grap[i].u;
int v = grap[i].v;
if(findx(u) != findx(v)){
ans += grap[i].w;
link(findx(u),findx(v));
}
}
printf("%d\n",ans);
}
return 0;
}
//HDU 1102