# 数据结构与算法--最小生成树之Prim算法

• 最小生成树首先是一个生成树，所以我们研究的是无环连通分量；
• 边的权值可能是0也可能是负数
• 边的权值不一定表示距离，还可以是费用等

## 加权无向图的实现

``````package Chap7;

public class Edge implements Comparable<Edge> {
private int either;
private int other;
private double weight;

public Edge(int either, int other, double weight) {
this.either = either;
this.other = other;
this.weight = weight;

}

public double weight() {
return weight;
}

public int either() {
return either;
}

public int other(int v) {
if (v == either) {
return other;
} else if (v == other) {
return either;
} else throw new RuntimeException("该边无此顶点！");
}

@Override
public int compareTo(Edge that) {
return Double.compare(this.weight, that.weight);
}

@Override
public String toString() {
return "(" +
either +
"-" + other +
" " + weight +
')';
}
}
``````

Edge类实现了`Comparable<Edge>`，使得Edge本身可以进行比较（就像`Double`类那样）而比较的依据是边上的权值。Edge类中的`other(int v)`方法，接收一个顶点，如果v在该边中，返回该边的另一个顶点，否则抛出异常。

``````package Chap7;

import java.util.*;

public class EdgeWeightedGraph<Item> {

private int vertexNum;
private int edgeNum;
// 邻接表
// 顶点信息
private List<Item> vertexInfo;

public EdgeWeightedGraph(List<Item> vertexInfo) {
this.vertexInfo = vertexInfo;
this.vertexNum = vertexInfo.size();
for (int i = 0; i < vertexNum; i++) {
}
}

public EdgeWeightedGraph(List<Item> vertexInfo, int[][] edges, double[] weight) {
this(vertexInfo);
for (int i = 0; i < edges.length;i++) {
Edge edge = new Edge(edges[i][0], edges[i][1], weight[i]);
}
}

public EdgeWeightedGraph(int vertexNum) {
this.vertexNum = vertexNum;
for (int i = 0; i < vertexNum; i++) {
}
}

public EdgeWeightedGraph(int vertexNum, int[][] edges, double[] weight) {
this(vertexNum);
for (int i = 0; i < edges.length;i++) {
Edge edge = new Edge(edges[i][0], edges[i][1], weight[i]);
}
}

int v = edge.either();
int w = edge.other(v);
edgeNum++;
}
// 返回与某个顶点依附的所有边
}

public List<Edge> edges() {
for (int i = 0; i < vertexNum; i++) {
// i肯定是边e的一个顶点，我们只取other大于i的边，避免添加重复的边
if (e.other(i) > i) {
}
}
}
return edges;
}

public int vertexNum() {
return vertexNum;
}

public int edgeNum() {
return edgeNum;
}

public Item getVertexInfo(int i) {
return vertexInfo.get(i);
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(vertexNum).append("个顶点, ").append(edgeNum).append("条边。\n");
for (int i = 0; i < vertexNum; i++) {
}
return sb.toString();
}

public static void main(String[] args) {
List<String> vertexInfo = Arrays.asList("v0", "v1", "v2", "v3", "v4");
int[][] edges = {{0, 1}, {0, 2}, {0, 3},
{1, 3}, {1, 4},
{2, 4}};
double[] weight = {30.0, 40.0, 20.5, 10.0, 59.5, 20.0};

EdgeWeightedGraph<String> graph = new EdgeWeightedGraph<>(vertexInfo, edges, weight);
System.out.println("该图的邻接表为\n"+graph);
System.out.println("该图的所有边："+ graph.edges());

}
}

``````

`edges()`方法可以返回图中的所有边。下面的判断比较关键

``````for (Edge e: adj(i)) {
if (e.other(i) > i) {
}
}
``````

## Prim算法

• 从某一个顶点开始（不妨假设从顶点0开始），标记它，并将它邻接表中的边全部加入到队列中；
• 从队列中选出并删除权值最小的那条边，先检查这条边的两个顶点是否都已经被标记过，若是，加入这条边会导致成环。跳过这条边，选择并删除下一个权值最小的边，直到某条边的两个顶点不是都被标记过，然后将其加入到MST中，将该边的另一个顶点标记，并将所有与这个顶点相邻且未被标记的顶点的边加入队列。
• 重复上述步骤，直到列表中的元素都被删除。
``````package Chap7;

import java.util.*;

public class LazyPrim {
private boolean marked[];
Queue<Edge> edges;
private Queue<Edge> mst;

public LazyPrim(EdgeWeightedGraph<?> graph) {
marked = new boolean[graph.vertexNum()];
edges = new PriorityQueue<>();
// 从顶点0开始访问
visit(graph, 0);
// 只要边还没被删除完，就循环
while (!edges.isEmpty()) {
// 优先队列,将权值最小的选出并删除
Edge edge = edges.poll();
int v = edge.either();
int w = edge.other(v);
// 这样的边会导致成环，跳过
if (marked[v] && marked[w]) {
continue;
}
// 加入到MST中
mst.offer(edge);
// 因为edges中的边肯定是有一个顶点已经visit过了，但是不知道是either还是other
// 如果v没被标记，那么访问它；否则v被标记了，那么w肯定没被标记（marked[v] && marked[w]的情况已经被跳过了）
if (!marked[v]) {
visit(graph, v);
} else {
visit(graph, w);
}
}
}

private void visit(EdgeWeightedGraph<?> graph, int v) {
marked[v] = true;
for (Edge e : graph.adj(v)) {
// v的邻接边中，将另一个顶点未被标记的边加入列表中。若另一个顶点标记了还加入，就会重复添加
if (!marked[e.other(v)]) {
edges.offer(e);
}
}
}

public Iterable<Edge> edges() {
return mst;
}

public double weight() {
return mst.stream().mapToDouble(Edge::weight).sum();
}

public static void main(String[] args) {
List<String> vertexInfo = Arrays.asList("v0", "v1", "v2", "v3", "v4", " v5", "v6", "v7");
int[][] edges = {{4, 5}, {4, 7}, {5, 7}, {0, 7},
{1, 5}, {0, 4}, {2, 3}, {1, 7}, {0, 2}, {1, 2},
{1, 3}, {2, 7}, {6, 2}, {3, 6}, {6, 0}, {6, 4}};

double[] weight = {0.35, 0.37, 0.28, 0.16, 0.32, 0.38, 0.17, 0.19,
0.26, 0.36, 0.29, 0.34, 0.40, 0.52, 0.58, 0.93};

EdgeWeightedGraph<String> graph = new EdgeWeightedGraph<>(vertexInfo, edges, weight);
LazyPrim prim = new LazyPrim(graph);
System.out.println("MST的所有边为：" + prim.edges());
System.out.println("最小成本和为：" + prim.weight());
}
}

/* Outputs

MST的所有边为：[(0-7 0.16), (1-7 0.19), (0-2 0.26), (2-3 0.17), (5-7 0.28), (4-5 0.35), (6-2 0.4)]

*/
``````

• 从顶点0开始，标记它，并将0-7， 0-2， 0-6， 0-4加入edges。这体现在一开始的`visit(graph, 0)`
• 此时edges不为空，只要edges不为空，while循环就一直持续下去。选择并删除第一个元素（也就是权值最小的边0-7），加入到MST中。
• 接着访问顶点7，将其所有邻接边加入到edges中，选出1-7这条权值最小的边，加入到MST中。然后访问顶点1，将除了1-7外的其他和1邻接的边都加入到edges中，从edges中选出权值最小的边为0-2加入到MST，2的邻接边中2-7, 1-2由于会导致成环，所以不加入edges，将2-3， 2-6加入。
• 选择权值最小的2-3，加入MST。将3-6加入到edges。
• 选出权值最小的5-7加入MST，4-5加入edges。
• 接下来1-3,1-5,2-7由于两个顶点都被标记过，所以被跳过。选择4-5加入MST，同时6-4加入edges。
• 1-2， 4-7，0-4连个顶点被标记，跳过。选择6-2加入MST。至此n个顶点和n - 1条边都被加入到MST中，最小生成树完成了。
• 后续工作，edges中剩余的边，因为两个顶点都标记过，所以一直跳过直到edges为空，程序结束。

## Prim算法的优化

0为起点，一开始，0-4、0-7、0-2、0-6会加入到edges，然后选出权值最小的0-7边。关键来了，延时实现中，会将7-1， 7-2， 7-5， 7-4全加入edges。我们知道7-2和7-4最后因为是无效边会被跳过。0和7都已经在MST中，那么7-4和0-4两条边不可能被同时选出作为MST的边，否则成环；所以两者只能有一条有可能成为MST的边，自然选权值小的那条啊！所以到顶点4的边应该选7-4，但是0-4已经被加入到edges中，我们要做的就是用7-4取代0-4；再看另一边，同样7-2和0-2也是只有一条有可能作为MST的边，但是已经加入edges的0-2权值本来就比7-2要小，所以7-2不能加入到edges。优化后的Prim算法在这一步中只将7-1和7-5存入，加上一次将0-4修改为7-4的操作。

``````package Chap7;

import java.util.*;

public class Prim {
private boolean marked[];
private Edge[] edgeTo;
private double[] distTo;
private Map<Integer, Double> minDist;

public Prim(EdgeWeightedGraph<?> graph) {
marked = new boolean[graph.vertexNum()];
edgeTo = new  Edge[graph.vertexNum()];
distTo = new double[graph.vertexNum()];
minDist = new HashMap<>();
// 初始化distTo，distTo[0]不会被赋值，默认0.0正好符合我们的要求，使余下的每个值都为正无穷,
for (int i = 1; i < graph.vertexNum(); i++) {
distTo[i] = Double.POSITIVE_INFINITY; // 1.0 / 0.0为INFINITY
}

visit(graph, 0);
while (!minDist.isEmpty()) {
visit(graph, delMin());
}
}

private int delMin() {
Set<Map.Entry<Integer, Double>> entries = minDist.entrySet();
Map.Entry<Integer, Double> min = entries.stream().min(Comparator.comparing(Map.Entry::getValue)).get();
int key = min.getKey();
minDist.remove(key);
return key;
}

private void visit(EdgeWeightedGraph<?> graph, int v) {
marked[v] = true;
int w = e.other(v);
if (marked[w]) {
continue;
}

if (e.weight() < distTo[w]) {
distTo[w] = e.weight();
edgeTo[w] = e;
if (minDist.containsKey(w)) {
minDist.replace(w, distTo[w]);
} else {
minDist.put(w, distTo[w]);
}
}
}

}

public Iterable<Edge> edges() {
List<Edge> edges = new ArrayList<>();
return edges;
}

public double weight() {
return Arrays.stream(distTo).reduce(0.0, Double::sum);
}

public static void main(String[] args) {
List<String> vertexInfo = Arrays.asList("v0", "v1", "v2", "v3", "v4", " v5", "v6", "v7");
int[][] edges = {{4, 5}, {4, 7}, {5, 7}, {0, 7},
{1, 5}, {0, 4}, {2, 3}, {1, 7}, {0, 2}, {1, 2},
{1, 3}, {2, 7}, {6, 2}, {3, 6}, {6, 0}, {6, 4}};

double[] weight = {0.35, 0.37, 0.28, 0.16, 0.32, 0.38, 0.17, 0.19,
0.26, 0.36, 0.29, 0.34, 0.40, 0.52, 0.58, 0.93};

EdgeWeightedGraph<String> graph = new EdgeWeightedGraph<>(vertexInfo, edges, weight);
Prim prim = new Prim(graph);
System.out.println("MST的所有边为：" + prim.edges());
System.out.println("最小成本和为：" + prim.weight());
}
}

/* Outputs

MST的所有边为：[(1-7 0.19), (0-2 0.26), (2-3 0.17), (4-5 0.35), (5-7 0.28), (6-2 0.4), (0-7 0.16)]

*/
``````

• 首先访问顶点0，边0-7, 0-2, 0-4, 0-6被加入Map中，因为这些边是目前（唯一）MST外顶点与MST连接的最小权值边，也就是说edgeTo[7] = 0-7、edgeTo[2] = 0-2、edgeTo[4] = 0-4、edgeTo[6] = 0-6。
• 然后删除权值最小的边0-7，并开始访问顶点7，将7-5, 7-1加入Map，7-4因为比0-4权值小（更靠近MST），所以edgeTo[4] 改为 7-4；7-2不加入Map因为0-2的权值本来就比7-2小。现在edgeTo[5] = 7-5, edgeTo[1] = 7-1;
• 删除边7-1，并访问顶点1。将1-3加入Map，1-5不加入因为7-5的权值本来就比它小。edgeTo[3] = 1-3
• 删除0-2，并访问顶点2，2-3的权值比1-3小，所以更新edgeTo[3] = 2-3，2-6权值小于0-6，更新edgeTo[6] = 2-6
• 删除2-3，并访问顶点3，3-6不加入Map因为2-6权值本来就比它小。
• 删除5-7，5-4权值比7-4小，更新edgeTo[4] = 5-4
• 删除4-5，访问顶点4，4-6不加入Map，因为2-6的权值本来就比它小
• 删除6-2，至此所有顶点都已访问过，且Map为空，最小生成树完成。

Prim算法，就好比：从一株小树苗开始，不断从附近找到离它最近的一根树枝，安在自己身上，小树慢慢长大成大树，最后找到n-1条树枝后就成了最小生成树。

by @sunhaiyu

2017.9.21