图片来自:勿在浮沙筑高台http://blog.csdn.net/luoshixian099/article/details/51908175
一、前言
在了解图的最小生成树之前,先弄清几种图的概念:
- 连通图:在无向图中,若任意两个顶点vivi与vjvj都有路径相通,则称该无向图为连通图。
- 强连通图:在有向图中,若任意两个顶点vivi与vjvj都有路径相通,则称该有向图为强连通图。
- 连通网:在连通图中,若图的边具有一定的意义,每一条边都对应着一个数,称为权;权代表着连接连个顶点的代价,称这种连通图叫做连通网。
- 生成树:一个连通图的生成树是指一个连通子图,它含有图中全部n个顶点,但只有足以构成一棵树的n-1条边。一颗有n个顶点的生成树有且仅有n-1条边,如果生成树中再添加一条边,则必定成环。
- 最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。
二、两种最小生成树算法
1.Prim算法
此算法可以称为“加点法”,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。
步骤:
- 图的所有顶点集合为V;初始令集合u={s},v=V−u
- 在两个集合u,v能够组成的边中,选择一条代价最小的边(u0,v0),加入到最小生成树中,并把v0并入到集合u中
- 重复上述步骤,直到最小生成树有n-1条边或者n个顶点为止
需要将代价最小的边对应的点加入到最小生成树(集合u)中,所以需要一个辅助数组dis[],用来维护每个顶点到最小生成树(集合u)最小代价边信息
代码实现
private void prim_(EdgeWeightGraph G, int v) {
int count = 0;
int min,minVertex = 0;
// 标记起点,并加入集合u(mst)
marked[v] = true;
mst.add(v);
++count;
while (count < G.getV()) {
// 查找与生成树不连接,且边权最小的顶点
min = Integer.MAX_VALUE;
for (int i = 1; i < dis.length; i++) {
if (!marked[i] && dis[i] < min) {
min = dis[i];
minVertex = i;
}
}
marked[minVertex] = true;
mst.add(minVertex);
sum += dis[minVertex];
++count;
// 加入新的顶点,更新dis数组
Vertex vertex = G.getVertex(minVertex);
for (int i = 0; i < vertex.size(); i++) {
Edge edge = vertex.get(i); // 相邻的边
int nextVertex = edge.getVertexIndex(); // 相邻的顶点
int nextWeight = edge.getWeight(); // 与相邻顶点之间的边权
if (!marked[nextVertex] && nextWeight < dis[nextVertex])
dis[nextVertex] = nextWeight;
}
}
}
时间复杂度:O(e*v)
使用优先队列优化Prim算法
步骤:
- 访问起点s,将与之相连的边加入到优先队列
- 每次取到代价最小的边,如果边的两个顶点都被访问,那么边失效;否则,访问为标记的顶点,并加入的最小生成树(集合u)
- 重复步骤2,直到队列为空
private void prim_(EdgeWeightGraph G, int v) {
PriorityQueue<Edge> queue = new PriorityQueue<>(new Comparator<Edge>() {
@Override
public int compare(Edge o1, Edge o2) {
return o1.getWeight() - o2.getWeight();
}
});
visit(G, v, queue);
while (!queue.isEmpty()) {
// 取得边权最小的判断顶点是否访问过
Edge edge = queue.remove();
int v1 = edge.getpVertex();
int v2 = edge.getVertexIndex();
if (marked[v1] && marked[v2]) // 边失效
continue;
sum += edge.getWeight();
mst.add(edge);
if (!marked[v1])
visit(G, v1, queue);
if (!marked[v2])
visit(G, v2, queue);
}
}
// 将与顶点v关联(且没有访问过)的边添加到优先队列中
private void visit(EdgeWeightGraph G, int v, PriorityQueue quque) {
// 标记顶点v并将所有连接v和为标记顶点的边添加队列
marked[v] = true;
Vertex vertex = G.getVertex(v);
for (int i = 0; i < vertex.size(); i++) {
int nextVertex = vertex.get(i).getVertexIndex();
if (!marked[nextVertex])
quque.add(vertex.get(i));
}
}
使用了优先队列和邻接表优化之后时间复杂度:O(e*lge)
2.Kruskal算法
此算法可以称为“加边法”,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。
步骤:
- 把图中的所有边按代价从小到大排序
- 把图中的n个顶点看成独立的n棵树组成的森林
- 按权值从小到大选择边,所选的边连接的两个顶点ui,vi,应属于两颗不同的树,则成为最小生成树的一条边,并将这两颗树合并作为一颗树
- 重复(3),直到所有顶点都在一颗树内或者有n-1条边为止
主要问题是如何判断这个代价最小的边的两个顶点是否相通,这里我们使用并查集解决问题
点击了解并查集
代码如下:
扫描二维码关注公众号,回复:
10595360 查看本文章
private void Kruskal_(EdgeWeightGraph G, PriorityQueue<Edge> queue) {
int v = G.getV();
int count = 0;
while (!queue.isEmpty()) {
Edge edge = queue.remove();
int v1 = edge.getpVertex();
int v2 = edge.getVertexIndex();
// 判断两个顶点是否连通
if (merge(v1, v2)) {
// 如果为连通,那么就选这条边
++count;
mst.add(edge);
sum += edge.getWeight();
}
// 选择了n-1条边退出
if (count == v - 1)
break;
}
}
// 判断两个顶点是否为在同一集合,如果不在那么合并两个顶点
private boolean merge(int v, int w) {
int t1 = getF(v);
int t2 = getF(w);
if (t1 != t2) {
// 如果没在同一集合,约定t2的父顶点为t1
f[t2] = t1;
return true;
}
return false;
}
// 并查集寻找顶点的父顶点
private int getF(int v) {
if (f[v] == v) {
return v;
} else {
// 更新父顶点 - 路径压缩
f[v] = getF(f[v]);
return f[v];
}
}