加权无向图中的边不能简单的使用v-w两个顶点表示了,而必须要给边 关联一个权重值,因此可以使用 对象 来描述一条边
- API
- 代码
package chapter17;
import chapter03.Queue;
/**
-
@author 土味儿
-
Date 2021/9/16
-
@version 1.0
-
加权无向图
*/
public class EdgeWeightedGraph {
/**
- 顶点
数量
*/
private final int vNum;
/**
- 边数量
*/
private int eNum;
/**
- 邻接表
*/
private Queue[] adj;
/**
-
构造器
-
@param vNum
*/
public EdgeWeightedGraph(int vNum) {
// 初始化顶点数量
this.vNum = vNum;
// 初始化边数量
this.eNum = 0;
// 初始化邻接表
this.adj = new Queue[vNum];
// 初始化邻接表中的空队列
for (int i = 0; i < vNum; i++) {
this.adj[i] = new Queue();
}
}
/**
-
得到顶点数量
-
@return
*/
public int getVNum(){
return vNum;
}
/**
-
得到边数量
-
@return
*/
public int geteNum(){
return eNum;
}
/**
-
添加一条边v-w
-
@param e
*/
public void addEdge(Edge e){
// 因为是无向图,让边e同时出现在e的两个顶点的邻接表中
int v = e.either();
int w = e.other(v);
this.adj[v].enQueue(e);
this.adj[w].enQueue(e);
// 边数量加1
eNum++;
}
/**
-
获取顶点v的所有相邻顶点
-
@param v
-
@return
*/
public Queue adj(int v){
return this.adj[v];
}
/**
-
获取加权无向图中的所有边
-
@return
*/
public Queue edges(){
// 创建一个队列对象,存储所有的边
Queue allEdges = new Queue<>();
// 遍历图中的每一个顶点,找到每个顶点的邻接表,邻接表中存储了该顶点关联的每一条边
for(int v=0;v<vNum;v++){
// 遍历顶点v的邻接表,找到每一条和v关联的边
for (Edge e : adj(v)) {
// 每条边的两个顶点,一大一小,判断大小再添加,可以避免重复
if(e.other(v) < v){
allEdges.enQueue(e);
}
}
}
return allEdges;
}
}
==================================================================
之前学习的加权图,它的边关联了一个权重,那么可以根据这个权重解决最小成本问题,但如何才能找到最小成本对应的顶点和边呢?最小生成树相关算法可以解决。
- 图的生成树 是它的一棵含有其 所有顶点 的 无环连通子图,一副加权无向图的最小生成树,是它的一棵权值(树中所有边的权重之和)最小的生成树
- 只考虑连通图
最小生成树的定义说明它只能存在于连通图中, 如果图不是连通的,那么分别计算每个连通图子图的最小生成树,合并到一起称为 最小生成森林
- 所有边的权重都各不相同
如果不同的边权重可以相同,那么一副图的最小生成树就可能不唯一了,虽然算法可以处理这种情况,但为了好理解,约定所有边的权重都各不相同
1)树的性质
- 用一条边连接树中的任意两个顶点都会产生 一个新的环
- 从树中删除任意一条边,将会得到 两棵独立的树
2)切分定理
要从一副连通图中找出该图的最小生成树,需要通过切分定理完成
- 切分
将图的所有顶点按照某些规则分为两个 非空 且 没有交集 的集合
- 横切边
连接两个属于不同集合的顶点的边称之为横切边
例如:
将图中的顶点切分为两个集合,灰色顶点属于一个集合,白色顶点属于另外一个集合,那么效果如下:
- 切分定理
在一副加权图中,给定任意的切分,它的 横切边中的权重最小者 必然属于图中的最小生成树
- 注意
一次切分产生的多个横切边中,权重最小的边不一定是所有横切边中唯一属于图的最小生成树的边
3)贪心算法
贪心算法是计算图的最小生成树的基础算法,它的基本原理就是 切分定理
使用切分定理找到最小生成树的一条边,不断的重复直到找到最小生成树的所有边
如果图有 V 个顶点,那么需要找到 V-1 条边,就可以表示该图的最小生成树
计算图的最小生成树的算法有很多种,但这些算法都可以看做是贪心算法的一种特殊情况,这些算法的不同之处在于 保存切分 和 判定权重最小的横切边 的方式
4)Prim算法
Prim算法,它的每一步都会为一棵生成中的树添加一条边。一开始这棵树只有一个顶点,然后会向它添加V-1条边,每次总是将下一条连接树中的顶点与不在树中的顶点且权重最小的边加入到树中
- Prim算法的切分规则
把最小生成树中的顶点看做是一个集合,把不在最小生成树中的顶点看做是另外一个集合
1、Prim算法API设计
2、Prim算法的实现原理
Prim算法始终将图中的顶点切分成两个集合,最小生成树顶点 和 非最小生成树顶点,通过不断的重复做某些操作,可以逐渐将非最小生成树中的顶点加入到最小生成树中,直到所有的顶点都加入到最小生成树中
在设计API的时候,使用最小索引优先队列存放树中顶点与非树中顶点的有效横切边,那么它是如何表示的
呢?
可以让最小索引优先队列的 索引值表示图的顶点,让最小索引优先队列中的 值 表示从其他某个顶点到当前顶点的 边权重
初始化状态,先默认0是最小生成树中的唯一顶点,其他的顶点都不在最小生成树中,此时横切边就是顶点0的邻接表中0-2,0-4,0-6,0-7这四条边,只需要将索引优先队列的2、4、6、7索引处分别存储这些边的权重值就可以表示了。
现在只需要从这四条横切边中找出权重最小的边,然后把对应的顶点加进来即可。所以找到0-7这条横切边的权重最小,因此把0-7这条边添加进来,此时0和7属于最小生成树的顶点,其他的不属于,现在顶点7的邻接表中的边也成为了横切边,这时需要做两个操作:
1、0-7这条边已经不是横切边了,需要让它失效:只需要调用最小索引优先队列的delMin()方法即可完成
2、2和4顶点各有两条连接指向最小生成树,需要只保留一条:4-7的权重小于0-4的权重,所以保留4-7,调用索引优先队列的change(4,0.37)即可,0-2的权重小于2-7的权重,所以保留0-2,不需要做额外操作
不断重复上面的动作,就可以把所有的顶点添加到最小生成树中
3、代码
package chapter18;
import chapter03.Queue;
import chapter07.IndexMinPriorityQueue;
import chapter17.Edge;
import chapter17.EdgeWeightedGraph;
/**
-
@author 土味儿
-
Date 2021/9/17
-
@version 1.0
-
最小生成树prim算法
*/
public class PrimMST {
/**
-
最小边
-
索引代表顶点
-
值表示当前顶点到最小生成树之间的最小边
*/
private Edge[] edgeTo;
/**
-
最小边的权重
-
索引代表顶点
-
值表示当前顶点到最小生成树之间的最小边的权重
*/
private double[] distTo;
/**
-
索引代表顶点
-
如果当前顶点已经在树中,则值为true,否则为false
*/
private boolean[] marked;
/**
- 存放树中顶点与非树中顶点的有效横切边
*/
private IndexMinPriorityQueue pq;
/**
-
构造器
-
根据加权无向图创建最小生成树
-
@param g
*/
public PrimMST(EdgeWeightedGraph g) {
// 初始化edgeTo
this.edgeTo = new Edge[g.getVNum()];
// 初始化distTo
this.distTo = new double[g.getVNum()];
for (int i = 0; i < g.getVNum(); i++) {
// 默认值:正无穷大
this.distTo[i] = Double.POSITIVE_INFINITY;
}
// 初始化marked
this.marked = new boolean[g.getVNum()];
// 初始化pq
pq = new IndexMinPriorityQueue(g.getVNum());
// 默认让顶点0进入到树中,但是树中只有一个顶点0,因此,顶点0没有和任何顶点相连,所以distTo对应位置处的值为0.0
distTo[0] = 0.0;
pq.insert(0,0.0);
// 遍历索引最小优先队列,拿到最小横切边对应的顶点,把该顶点加入到最小树中
while(!pq.isEmpty()){
visit(g, pq.delMin());
}
}
/**
-
获取最小生成树的所有边
-
@return
*/
public Queue edges(){
// 创建队列对象
Queue allEdges = new Queue<>();
// 把edgeTo中非null的值加入队列
for (int i = 0; i < edgeTo.length; i++) {
if(edgeTo[i] != null){
allEdges.enQueue(edgeTo[i]);
}
}
return allEdges;
}
/**
-
将顶点v添加到最小生成树中,并更新数据
-
@param g
-
@param v
*/
private void visit(EdgeWeightedGraph g,int v){
// 把顶点v添加到最小生成树中
marked[v] = true;
// 更新数据
for (Edge e : g.adj(v)) {
// 获取边e的另外一个顶点w(当前顶点v)
int w = e.other(v);
// 判断另一个顶点是否在树中,如果在树中,不做处理;如果不在树中,更新数据
if(marked[w]){
// 继续下次循环;不是退出,不能用break
continue;
}
// 比较权重:判断边e的权重是否小于 w到最小树中已存在的最小边的权重
if(e.getWeight() < distTo[w]){
// 更新数据
edgeTo[w] = e;
distTo[w] = e.getWeight();
if(pq.contains(w)){
pq.changeItem(w, e.getWeight());
}else{
pq.insert(w, e.getWeight());
}
}
}
}
}
- 测试
package chapter18;
import chapter03.Queue;
import chapter17.Edge;
import chapter17.EdgeWeightedGraph;
import org.junit.Test;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
-
@author 土味儿
-
Date 2021/9/17
-
@version 1.0
-
测试prim最小生成树
*/
public class PrimMSTTest {
@Test
public void test() throws IOException {
// 准备一幅加权无向图
BufferedReader br = new BufferedReader(new InputStreamReader(PrimMSTTest.class.getClassLoader().getResourceAsStream(“min_create_tree_test.txt”)));
// 顶点数量
int total = Integer.parseInt(br.readLine());
// 加权无向图
EdgeWeightedGraph g = new EdgeWeightedGraph(total);
// 边的数量
int edges = Integer.parseInt(br.readLine());
// 依次读取边
for (int i = 0; i < edges; i++) {
String line = br.readLine();
String[] s = line.split(" ");
int v = Integer.parseInt(s[0]);
int w = Integer.parseInt(s[1]);
double weight = Double.parseDouble(s[2]);
// 创建加权无向边
Edge edge = new Edge(v, w, weight);
// 向图中加入边
g.addEdge(edge);
}
// 创建prim最小生成树对象
PrimMST primMST = new PrimMST(g);
// 得到最小生成树
Queue allEdges = primMST.edges();
// 输出
for (Edge e : allEdges) {
int x = e.either();
int y = e.other(x);
double w = e.getWeight();
System.out.println(x + " - " + y + " : " + w);
}
}
}
- min_create_tree_test.txt
8
16
4 5 0.35
4 7 0.37
5 7 0.28
0 7 0.16
1 5 0.32
0 4 0.38
2 3 0.17
1 7 0.19
0 2 0.26
1 2 0.36
1 3 0.29
2 7 0.34
6 2 0.40
3 6 0.52