请看如下的示例图,该图有 V1-V7 七个顶点,每个顶点之间的距离如图所示:
如果上面的图为七个城市的地理分布图,城市间相连的边上的数字为城市间的距离。我们要在这七个城市里面架设电线,使得每一个城市都在电网之中,怎么样架设电线,可以使得所使用的电线最短呢?
这就是图论里面的最小生成树的问题,今天我们用普利姆(Prim)算法,来求得电线架设的方案。话不多说,精髓全在代码和其间的注释里,可复制到IDE中运行:
1、图的顶点类,包含一到多个边:
import java.util.HashSet;
import java.util.Set;
/**
* 图的顶点类
* @Author: LiYang
* @Date: 2019/9/8 21:32
*/
public class Vertex {
//顶点的数据
public String value;
//顶点的边的集合(其相邻的顶点和权重)
public Set<Edge> neighbors = new HashSet<>();
/**
* 空构造方法
*/
public Vertex(){
}
/**
* 构造方法,输入顶点值
* @param value 顶点值
*/
public Vertex(String value) {
this.value = value;
}
/**
* 为该顶点加入边
* @param neighbor 边的另一顶点
* @param weight 边的权重
*/
public void addEdge(Vertex neighbor, int weight){
neighbors.add(new Edge(neighbor, weight));
}
/**
* 重新toString()方法,打印顶点的值
* @return
*/
@Override
public String toString() {
return value;
}
}
2、图的边类,包含目的顶点,以及权重(距离):
/**
* 图的边类,带权
* Edge是属于某个顶点的,所以出发点是该顶点
* Edge属性只有目的点,以及权重
* @Author: LiYang
* @Date: 2019/9/8 21:32
*/
public class Edge {
//边的终点(邻居)
public Vertex neighbor;
//权重(距离)
public int weight;
/**
* 无参构造
*/
public Edge(){
}
/**
* 全参数构造
* @param neighbor 邻接点
* @param weight 权重
*/
public Edge(Vertex neighbor, int weight) {
this.neighbor = neighbor;
this.weight = weight;
}
}
3、普利姆(Prim)算法表类,用于动态统计距离和顶点关系的表
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 普利姆(Prim)算法的表
* @Author: LiYang
* @Date: 2019/9/8 22:01
*/
public class PrimTable{
//顶点
public Vertex vertex;
//是否已访问
public boolean known;
//距离
public int distance;
//上一顶点
public Vertex previous;
/**
* 初始化普利姆(Prim)算法表
* @param vList 顶点集合
* @return 普利姆(Prim)算法表
*/
public static Map<String, PrimTable> initialPrimTable(List<Vertex> vList){
//最后返回的普利姆(Prim)算法表
Map<String, PrimTable> primTableMap = new HashMap<>();
//开始循环添加(默认第一个顶点为开始伸展的起点)
for (Vertex vertex : vList){
//当前节点对应的普利姆(Prim)算法表的行
PrimTable primTable = new PrimTable();
//顶点
primTable.vertex = vertex;
//初始化为未访问
primTable.known = false;
//初始化距离为无穷大
primTable.distance = Integer.MAX_VALUE;
//上一顶点初始化置空
primTable.previous = null;
//以顶点名为键,放入Map中
primTableMap.put(vertex.value, primTable);
}
//返回初始化好的普利姆(Prim)算法表
return primTableMap;
}
/**
* 寻找普利姆(Prim)算法表中距离最小的value的key
* @param primTableMap 待查找的普利姆(Prim)算法表
* @return 普利姆(Prim)算法表最小距离的key
*/
public static String getMinimumPrimTableKey(Map<String, PrimTable> primTableMap){
//最小距离
int minimunDistance = Integer.MAX_VALUE;
//第一次遍历普利姆(Prim)算法表,找出最小值
for (Map.Entry<String, PrimTable> item : primTableMap.entrySet()){
//如果已经访问过,则跳过
if (item.getValue().known){
continue;
}
//得到当前的距离
int currentDistance = item.getValue().distance;
//更新最小值
if (minimunDistance > currentDistance){
minimunDistance = currentDistance;
}
}
//根据找到的最小值,找到最小值的key
for (Map.Entry<String, PrimTable> item : primTableMap.entrySet()){
//如果已经访问过,则跳过
if (item.getValue().known){
continue;
}
//得到当前的距离
int currentDistance = item.getValue().distance;
//如果当前距离就是最小距离,则返回其key
if (currentDistance == minimunDistance){
return item.getKey();
}
}
//如果找不到,则表示普利姆(Prim)算法终止
//因为算法while循环中加入了判断,所以不会到这里
return "PrimFinished";
}
/**
* 重新toString()方法,方便调试查看数据
* @return
*/
@Override
public String toString() {
return "PrimTable{" +
"vertex=" + vertex +
", known=" + known +
", distance=" + distance +
", previous=" + previous +
'}';
}
}
4、图类,里面包含创建上面示例图的方法,以及普利姆(Prim)算法求最小生成树的方法,还有运行的main方法:
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 数据结构与算法之:图
* @Author: LiYang
* @Date: 2019/9/8 21:31
*/
public class Graph {
/**
* 构建普利姆(Prim)算法所需要的图(本文刚开始的示例图)
* 注意,普利姆(Prim)算法需要连通图
* @return 图的顶点的集合
*/
public static List<Vertex> buildPrimGraph(){
//创建顶点
Vertex vertex1 = new Vertex("V1");
Vertex vertex2 = new Vertex("V2");
Vertex vertex3 = new Vertex("V3");
Vertex vertex4 = new Vertex("V4");
Vertex vertex5 = new Vertex("V5");
Vertex vertex6 = new Vertex("V6");
Vertex vertex7 = new Vertex("V7");
//为顶点创建边
vertex1.addEdge(vertex2, 2);
vertex1.addEdge(vertex3, 4);
vertex1.addEdge(vertex4, 1);
vertex2.addEdge(vertex1, 2);
vertex2.addEdge(vertex4, 3);
vertex2.addEdge(vertex5, 10);
vertex3.addEdge(vertex1, 4);
vertex3.addEdge(vertex4, 2);
vertex3.addEdge(vertex6, 5);
vertex4.addEdge(vertex1, 1);
vertex4.addEdge(vertex2, 3);
vertex4.addEdge(vertex3, 2);
vertex4.addEdge(vertex5, 7);
vertex4.addEdge(vertex6, 8);
vertex4.addEdge(vertex7, 4);
vertex5.addEdge(vertex2, 10);
vertex5.addEdge(vertex4, 7);
vertex5.addEdge(vertex7, 6);
vertex6.addEdge(vertex3, 5);
vertex6.addEdge(vertex4, 8);
vertex6.addEdge(vertex7, 1);
vertex7.addEdge(vertex4, 4);
vertex7.addEdge(vertex5, 6);
vertex7.addEdge(vertex6, 1);
//所有顶点的集合
List<Vertex> vList = new ArrayList<>();
vList.add(vertex1);
vList.add(vertex2);
vList.add(vertex3);
vList.add(vertex4);
vList.add(vertex5);
vList.add(vertex6);
vList.add(vertex7);
//返回所有顶点
return vList;
}
/**
* 普利姆(Prim)算法,求最小生成树的边
* @param vList 图的顶点的集合
*/
public static void primAlgorithm(List<Vertex> vList){
//先生成该顶点集合的普利姆(Prim)算法表
Map<String, PrimTable> primTable = PrimTable.initialPrimTable(vList);
//我们从第一个顶点开始遍历,则先把普利姆(Prim)算法表的第一个顶点的距离设置为0
primTable.get(vList.get(0).value).distance = 0;
//重要:普利姆(Prim)算法
while (isNotAllKnown(primTable)){
//寻找普利姆(Prim)算法表中最小距离的key(known为false中寻找最小)
String minimumKey = PrimTable.getMinimumPrimTableKey(primTable);
//先将该顶点标记为known
primTable.get(minimumKey).known = true;
//获得该顶点
Vertex currentVertex = primTable.get(minimumKey).vertex;
//访问该顶点的所有有边相邻的邻居
for (Edge item : currentVertex.neighbors){
//获得当前邻居的value
String neighborKey = item.neighbor.value;
//如果该邻居顶点已被访问,则跳过
if (primTable.get(neighborKey).known){
continue;
}
//如果该邻居顶点没有被访问,则先拿出普利姆(Prim)算法表里面的距离
int primDistance = primTable.get(neighborKey).distance;
//拿出到邻居路径的权重
int neighborDistance = item.weight;
//如果当前路径,也就是邻居路径小于普利姆(Prim)算法表里面的距离
if (neighborDistance < primDistance){
//需要更新邻居普利姆(Prim)算法表里面的距离
primTable.get(neighborKey).distance = neighborDistance;
//然后普利姆(Prim)算法表里面的上一顶点,更新为当前顶点
primTable.get(neighborKey).previous = currentVertex;
}
}
}
//上面的while循环结束后,普利姆(Prim)算法即终止,解读最终生成的普利姆(Prim)算法表
printNeedConnectedEdgeAndDistance(primTable);
}
/**
* 检查图的所有顶点是否全部已访问
* @param primTable 待检查的普利姆(Prim)算法表
* @return 是否不是全部已访问
*/
private static boolean isNotAllKnown(Map<String, PrimTable> primTable){
//将普利姆(Prim)算法表遍历
for (Map.Entry<String, PrimTable> item : primTable.entrySet()){
//如果找到了一个known为false的点
if (!item.getValue().known){
//证明不是全known,返回true
return true;
}
}
//遍历完了,还没有找到known为false的点,则证明全部known
return false;
}
/**
* 解读最后完成的普利姆(Prim)算法表,打印得到的最小生成树需要连接的点,以及总距离和其他信息
* @param primTable 最后完成的普利姆(Prim)算法表
*/
public static void printNeedConnectedEdgeAndDistance(Map<String, PrimTable> primTable){
//最小生成树总距离
long totalDistance = 0;
//将最后完成的普利姆(Prim)算法表遍历
for (Map.Entry<String, PrimTable> item : primTable.entrySet()){
//如果previous是null,则跳过
if (item.getValue().previous == null){
continue;
}
//打印出需要连接的两个顶点,以及该连接的距离
System.out.println("需要连接的两个顶点:" + item.getValue().previous + " -> " + item.getValue().vertex + ",连接距离为:" + item.getValue().distance);
//累加到总距离
totalDistance = totalDistance + item.getValue().distance;
}
//最后,打印出最小生成树的总距离
System.out.println("该图的最小生成树的总距离:" + totalDistance);
}
/**
* 运行普利姆(Prim)算法,求出示例图的最小生成树
* @param args
*/
public static void main(String[] args) {
//构建示例图
List<Vertex> vList = buildPrimGraph();
//运行普利姆(Prim)算法,需要再控制台查看打印结果
primAlgorithm(vList);
}
}
5、运行main方法,控制台输出最小生成树的构建方法,以及总距离(城市间电线的架设方案,以及架设电线总长度):
需要连接的两个顶点:V7 -> V6,连接距离为:1
需要连接的两个顶点:V4 -> V7,连接距离为:4
需要连接的两个顶点:V1 -> V2,连接距离为:2
需要连接的两个顶点:V4 -> V3,连接距离为:2
需要连接的两个顶点:V1 -> V4,连接距离为:1
需要连接的两个顶点:V7 -> V5,连接距离为:6
该图的最小生成树的总距离:16
6、根据上面的方案,我们连接 V7 -> V6、V4 -> V7、V1 -> V2、V4 -> V3、V1 -> V4、V7 -> V5 六条边,得到下面的图,下面的图就是上面示例图的最小生成树,也就是架设电线的最优方案,总距离为16个单位(方案不一定唯一,但总距离一定是最小的,你找不到总距离比下图还小的电线架设方案了):
7、我们回顾之前的示例图,方便作对比查看: