欢迎访问https://blog.csdn.net/lxt_Lucia~~
宇宙第一小仙女\(^o^)/~~萌量爆表求带飞=≡Σ((( つ^o^)つ~dalao们点个关注呗~~
先介绍几个关于图的几个概念定义:
- 连通图:在无向图中,若任意两个顶点vi与vj都有路径相通,则称该无向图为连通图。
- 强连通图:在有向图中,若任意两个顶点vi与vj都有路径相通,则称该有向图为强连通图。
- 连通网:在连通图中,若图的边具有一定的意义,每一条边都对应着一个数,称为权;权代表着连接连个顶点的代价,称这种连通图叫做连通网。
- 生成树:一个连通图的生成树是指一个连通子图,它含有图中全部n个顶点,但只有足以构成一棵树的n-1条边。一颗有n个顶点的生成树有且仅有n-1条边,如果生成树中再添加一条边,则必定成环。
- 最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。
emmmmmm...... 今天师哥讲的时候,说最小生成树除了Kruskal和Prim还有一种,Sollin(Boruvka),但是很麻烦很不常用。
三种算法如下:
1.Kruskal算法(相对最好)
此算法可以称为“加边法”,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。
1. 把图中的所有边按代价从小到大排序;
2. 把图中的n个顶点看成独立的n棵树组成的森林;
3. 按权值从小到大选择边,所选的边连接的两个顶点ui,vi
,应属于两颗不同的树,则成为最小生成树的一条边,并将这两颗树合并作为一颗树。
4. 重复(3),直到所有顶点都在一颗树内或者有n-1条边为止。
初始情况,一个连通图,定义针对边的数据结构,包括起点,终点,边长度:
typedef struct _node{
intval; //长度
intstart; //边的起点
intend; //边的终点
}Node;
举个栗子好了,例图如下:
代码实现:
#include<iostream>
#define N 7
using namespace std;
typedef struct _node{
intval;
intstart;
intend;
}Node;
Node V[N];
intcmp(constvoid *a, constvoid *b)
{
return(*(Node *)a).val - (*(Node*)b).val;
}
intedge[N][3] = { { 0,1,3},
{0,4,1},
{1,2,5},
{1,4,4},
{2,3,2},
{2,4,6},
{3,4,7}
};
intfather[N] = { 0, };
intcap[N] = {0,};
voidmake_set() //初始化集合,让所有的点都各成一个集合,每个集合都只包含自己
{
for(inti = 0; i < N; i++)
{
father[i] = i;
cap[i] = 1;
}
}
intfind_set(intx) //判断一个点属于哪个集合,点如果都有着共同的祖先结点,就可以说他们属于一个集合
{
if(x != father[x])
{
father[x] = find_set(father[x]);
}
returnfather[x];
}
voidUnion(intx, inty) //将x,y合并到同一个集合
{
x = find_set(x);
y = find_set(y);
if(x == y)
return;
if(cap[x] < cap[y])
father[x] = find_set(y);
else
{
if(cap[x] == cap[y])
cap[x]++;
father[y] = find_set(x);
}
}
intKruskal(intn)
{
intsum = 0;
make_set();
for(inti = 0; i < N; i++)//将边的顺序按从小到大取出来
{
if(find_set(V[i].start) != find_set(V[i].end)) //如果改变的两个顶点还不在一个集合中,就并到一个集合里,生成树的长度加上这条边的长度
{
Union(V[i].start, V[i].end); //合并两个顶点到一个集合
sum += V[i].val;
}
}
returnsum;
}
intmain()
{
for(inti = 0; i < N; i++) //初始化边的数据,在实际应用中可根据具体情况转换并且读取数据,这边只是测试用例
{
V[i].start = edge[i][0];
V[i].end = edge[i][1];
V[i].val = edge[i][2];
}
qsort(V, N, sizeof(V[0]), cmp);
cout << Kruskal(0)<<endl;
Kruskal 模板
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n, m,sum;
struct node
{
int start,end,power;//start为起始点,end为终止点,power为权值
} edge[5050];
int pre[5050];
int cmp(node a, node b)
{
return a.power<b.power;//按照权值排序
}
int find(int x)//并查集找祖先
{
if(x!=pre[x])
{
pre[x]=find(pre[x]);
}
return pre[x];
}
void merge(int x,int y,int n)//并查集合并函数,n是用来记录最短路中应该加入哪个点
{
int fx=find(x);
int fy=find(y);
if(fx!=fy)
{
pre[fx]=fy;
sum+=edge[n].power;
}
}
int main()
{
while(~scanf("%d", &n), n)//n是点数
{
sum=0;
m=n*(n-1)/2;//m是边数,可以输入
int i;
int start,end,power;
for(i=1; i<=m; i++)
{
scanf("%d %d %d", &start, &end, &power);
edge[i].start=start,edge[i].end=end,edge[i].power=power;
}
for(i=1; i<=m; i++)
{
pre[i]=i;
}//并查集初始化
sort(edge+1, edge+m+1,cmp);
for(i=1; i <= m; i++)
{
merge(edge[i].start,edge[i].end,i);
}
printf("%d\n",sum);
}
return 0;
}
2.Prim算法(基于贪心)
此算法可以称为“加点法”,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。
- 图的所有顶点集合为V
;初始令集合u={s},v=V−u;在两个集合u,v能够组成的边中,选择一条代价最小的边(u0,v0),加入到最小生成树中,并把v0
- 并入到集合u中。
- 重复上述步骤,直到最小生成树有n-1条边或者n个顶点为止。
由于不断向集合u中加点,所以最小代价边必须同步更新;需要建立一个辅助数组closedge,用来维护集合v中每个顶点与集合u中最小代价边信息,:
struct
{
char vertexData //表示u中顶点信息
UINT lowestcost //最小代价
}closedge[vexCounts]
- 1
- 2
- 3
- 4
- 5
typedef struct _node{
intval; //长度
intstart; //边的起点
intend; //边的终点
}Node;
下面我们也举个栗子叭:
例图如下:
针对上面的图,代码如下:
#include<iostream>
#define INF 10000
using namespace std;
constint N = 6;
bool visit[N];
intdist[N] = { 0, };
intgraph[N][N] = { {INF,7,4,INF,INF,INF}, //INF代表两点之间不可达
{7,INF,6,2,INF,4},
{4,6,INF,INF,9,8},
{INF,2,INF,INF,INF,7},
{INF,INF,9,INF,INF,1},
{INF,4,8,7,1,INF}
};
intprim(intcur)
{
intindex = cur;
intsum = 0;
inti = 0;
intj = 0;
cout << index << " ";
memset(visit,false, sizeof(visit));
visit[cur] = true;
for(i = 0; i < N; i++)
dist[i] = graph[cur][i];//初始化,每个与a邻接的点的距离存入dist
for(i = 1; i < N; i++)
{
intminor = INF;
for(j = 0; j < N; j++)
{
if(!visit[j] && dist[j] < minor) //找到未访问的点中,距离当前最小生成树距离最小的点
{
minor = dist[j];
index = j;
}
}
visit[index] = true;
cout << index << " ";
sum += minor;
for(j = 0; j < N; j++)
{
if(!visit[j] && dist[j]>graph[index][j]) //执行更新,如果点距离当前点的距离更近,就更新dist
{
dist[j] = graph[index][j];
}
}
}
cout << endl;
returnsum; //返回最小生成树的总路径值
}
intmain()
{
cout << prim(0) << endl;//从顶点a开始
return0;
}
师哥讲的模板,看起来好像更熟悉一些:
#include<stdio.h>
#include<string.h>
#include <iostream>
#include <bits/stdc++.h>
#define IO ios::sync_with_stdio(false);\
cin.tie(0);\
cout.tie(0);
#define MAX 0x3f3f3f3f
using namespace std;
int logo[1010];//用来标记0和1 表示这个点是否被选择过
int map1[1010][1010];//邻接矩阵用来存储图的信息
int dis[1010];//记录任意一点到这个点的最近距离
int n;//点个数
int prim()
{
int i,j,now;
int sum=0;
/*初始化*/
for(i=1; i<=n; i++)
{
dis[i]=MAX;
logo[i]=0;
}
/*选定1为起始点,初始化*/
for(i=1; i<=n; i++)
{
dis[i]=map1[1][i];
}
dis[1]=0;
logo[1]=1;
/*循环找最小边,循环n-1次*/
for(i=1; i<n; i++)
{
now=MAX;
int min1=MAX;
for(j=1; j<=n; j++)
{
if(logo[j]==0&&dis[j]<min1)
{
now=j;
min1=dis[j];
}
}
if(now==MAX)
break;//防止不成图
logo[now]=1;
sum+=min1;
for(j=1; j<=n; j++)//添入新点后更新最小距离
{
if(logo[j]==0&&dis[j]>map1[now][j])
dis[j]=map1[now][j];
}
}
if(i<n)
printf("?\n");
else
printf("%d\n",sum);
}
int main()
{
while(scanf("%d",&n),n)//n是点数
{
int m=n*(n-1)/2;//m是边数
memset(map1,0x3f3f3f3f,sizeof(map1));//map是邻接矩阵存储图的信息
for(int i=0; i<m; i++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
if(c<map1[a][b])//防止重边
map1[a][b]=map1[b][a]=c;
}
prim();
}
}
HINT:
如果是想求最短路的话,只需要将被调中更新数组部分的两行代码改为:
if(logo[j]==0&&dis[j]>map1[now][j]+dis[now])
dis[j]=map1[now][j]+dis[now];
且此时min1和sum就没有用了,可以删掉。另外还需注意:其中表示循环的变量,有好几个都是从1开始的,如果纯粹用来计数,从0开始不会有影响,但是其他的不能改,在其他题目中,要参考其题意再决定。
Prim模板
#include<stdio.h>
#include<string.h>
#include <iostream>
#include <bits/stdc++.h>
#define MAX 0x3f3f3f3f
using namespace std;
int logo[1010];//用来标记0和1 表示这个点是否被选择过
int map1[1010][1010];//邻接矩阵用来存储图的信息
int dis[1010];//记录任意一点到这个点的最近距离
int n;//点个数
int prim()
{
int i,j,now;
int sum=0;
/*初始化*/
for(i=1; i<=n; i++)
{
dis[i]=MAX;
logo[i]=0;
}
/*选定1为起始点,初始化*/
for(i=1; i<=n; i++)
{
dis[i]=map1[1][i];
}
dis[1]=0;
logo[1]=1;
/*循环找最小边,循环n-1次*/
for(i=1; i<n; i++)
{
now=MAX;
int min1=MAX;
for(j=1; j<=n; j++)
{
if(logo[j]==0&&dis[j]<min1)
{
now=j;
min1=dis[j];
}
}
if(now==MAX)
break;//防止不成图
logo[now]=1;
sum+=min1;
for(j=1; j<=n; j++)//添入新点后更新最小距离
{
if(logo[j]==0&&dis[j]>map1[now][j])
dis[j]=map1[now][j];
}
}
if(i<n)
printf("?\n");
else
printf("%d\n",sum);
}
int main()
{
while(scanf("%d",&n),n)//n是点数
{
int m=n*(n-1)/2;//m是边数
memset(map1,0x3f3f3f3f,sizeof(map1));//map是邻接矩阵存储图的信息
for(int i=0; i<m; i++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
if(c<map1[a][b])//防止重边
map1[a][b]=map1[b][a]=c;
}
prim();
}
}
3.Sollin(Boruvka)算法
Sollin(Brouvka)算法虽然是最小生成树最古老的一个算法之一,其实是前面介绍两种算法的综合,每次迭代同时扩展多课子树,直到得到最小生成树T。
Sollin(Boruvka)算法步骤
1.用定点数组记录每个子树(一开始是单个定点)的最近邻居。(类似Prim算法)
2.对于每一条边进行处理(类似Kruskal算法)
如果这条边连成的两个顶点同属于一个集合,则不处理,否则检测这条边连接的两个子树,如果是连接这两个子树的最小边,则更新(合并)
由于每次循环迭代时,每棵树都会合并成一棵较大的子树,因此每次循环迭代都会使子树的数量至少减少一半,或者说第i次迭代每个分量大小至少为。所以,循环迭代的总次数为O(logn)。每次循环迭代所需要的计算时间:对于第2步,每次检查所有边O(m),去更新每个连通分量的最小弧;对于第3步,合并个子树。所以总的复杂度为O(E*logV)。
代码实现如下:
typedef struct{int v;int w;double wt;}Edge;
typeder struct{int V;int E;double **adj}Graph;
/*nn存储每个分量的最邻近,a存储尚未删除且还没在MST中的边
*h用于访问要检查的下一条边
*N用于存放下一步所保存的边
*每一步都对应着检查剩余的边,连接不同分量的顶点的边被保留在下一步中
*最后每一步将每个分量与它最邻近的分量合并,并将最近邻边添加到MST中
*/
Edge nn[maxE],a[maxE];
void Boruvka(Graph g,Edge mst[])
{
int h,i,j,k,v,w,N;
Edge e;
int E=GRAPHedges(a,G);
for(UFinit(G->V);E!=0;E=N)
{
for(k=0;k<G->V;k++)
nn[k]=Edge(G->V,G->V,maxWT);
for(h=0,N=0;h<E;h++)
{
i=find(a[h].v);j=find(a[h].w);
if(i==h) continue;
if(a[h].wt<nn[i].wt)nn[i]=a[h];
if(a[h].wt<nn[j].wt)nn[j]=a[h];
a[N++]=a[h];
}
for(k=0;k<G->V;k++)
{
e=nn[k];v=e.v;w=e.w;
if(v!=G->V&&!UFfind(v,w))
{
UFunion(v,w);mst[k]=e;
}
}
}
参考资料有:
http://www.cnblogs.com/aiguona/p/7223625.html(师哥博客)
https://blog.csdn.net/gettogetto/article/details/53216951
https://blog.csdn.net/luoshixian099/article/details/5190817
http://dsqiu.iteye.com/blog/1689178