加权无向图是一种为每条边关联一个权重值或是成本的图模型。这种图能够自然地表示许多应用。在一副航空图中,边表示航线,权值可以表示距离或是费用。在一副电路图中,边表示导线,权值表示导线的长度即成本,或是信号通过这条线所需的时间。此时我们很容易就能想到,最小成本的问题,例如,从西安飞纽约,怎样飞才能使时间成本最低或者使金钱成本最低?
在下图中,从顶点0到顶点4有三条路径,分别使0-2-3-4,0-2-4,0-5-3-4,哪我们如果要通过哪条路径到达4顶点最好呢?此时就要考虑,哪条路径的成本最低。
加权无向图中的边我们就不能简单的使用 v-2 两个顶点来表示了,而必须要给边关联一个权重值,因此我们可以使用对象来描述一条边。
类名 | Edge implements Comparable<Edge> |
---|---|
构造方法 | Edge(int v,int w,double weight):通过顶点v和w,以及权重weight值构造一个边对象 |
成员方法 | 1. public double weight():获取边的权重值2. public int either():获取边上的一个点3. public int other(int vertex):获取边上除了顶点vertex外的另一个顶点4. public int compareTo(Edge that):比较当前边和参数that边的权重,如果当前边权重大,返回1,如果一样大,返回0,如果当前权重小,返回-1 |
成员变量 | 1. private final int v:顶点一2. private final int w:顶点二3. private final double weight:当前边的权重 |
类名 | EdgeWeightGraph |
---|---|
构造方法 | EdgeWeightedGraph(int v):创建一个含有v个顶点的空加权无向图 |
成员方法 | 1. public int V():获取图中顶点的数量2. public int E():获取图中边的数量3. public void addEdge(Edge e):向加权无向图中添加一条边4. public Queue<Edge> adj(int v):获取和顶点v关联的所有边5. public Queue<Edge> edges():获取加权无向图的所有边 |
成员变量 | 1. private final int V:记录顶点数量2. private int E:记录边数量3. private Queue<Edge>[] adj:邻接表 |
之前学习的加权图,我们发现它的边关联的一个权重,那么我们就可以根据这个权重解决最小成本问题,但如何才能找对最小成本对应的顶点和边呢?最小生成树 可以解决
图的生成树是它的一颗含有其所有顶点的无环连通子图,一副加权无向图的最小生成树就是它的一颗权值(树中所有边的权重之和)最小的生成树
最小生成树 只考虑连通图,最小生成树的定义说明它只能存在于连通图中,如果图不是连通的,那么分别计算每个连通子图的最小生成树,合并到一起称为最小生成森林
所有边的权重都各不相同,如果不同的边权重可以相通,那么一副图的最小生成树就可能不唯一了,虽然我们的算法可以处理这种情况,但为了好理解,我们约定所有边的权重都各不相同
要从一副连通图中找出该图的最小生成树,需要通过切分定理完成。
切分:
横切边:
切分定理:
贪心算法是计算图的最小生成树的基础算法,它的基本原理就是切分定理,使用切分定理找到最小生成树的一条边,不断的重复直到找到最小生成树的所有边。
如果图有V个顶点,那么需要找 V-1 条边,就可以表示该图的最小生成树
计算图的最小生成树的算法有很多种,但这些算法都可以看作是贪心算法的一种特殊情况,这些算法的不同指出在于保存切分和判定权重最小的横切边方式
更多关于 贪心算法 的知识 可以在我的新一篇文章中查询到喔~!!
V :某顶点的邻接表中所有的顶点
计算最小生成树的算法:Prim算法,它的每一步都会为一棵生成的树添加一条边。一开始这棵树只有一个顶点,然后会向它添加 V-1条边,每次总是将下一条连接树中的顶点与不再树种的顶点且权重最小的边加入到树中。
把最小生成树中的顶点看作是一个集合,把不再最小生成树中的顶点看作是另外一个集合。
类名 | PrimMST |
---|---|
构造方法 | PrimMST(EdgeWeightedGraph G):根据一副加权无向图,创建最小生成树计算对象 |
成员方法 | 1. private void visit(EdgeWeightedGraph G,int V):将顶点v添加到最小生成树中,并且更新数据2. public Queue<Edge> edges():获取最小生成树的所有边 |
成员变量 | 1. private Edge[] edgeTo:索引代表顶点,值表示当前顶点和最小生成树之间的最短边2. private double[] distTo:索引代表顶点,值表示当前顶点和最小生成树之间的最短边的权重3. private boolean[] marked:索引代表顶点,如果当前顶点已经在树中,则值为true,否则为false4. private indexMinPriorityQueue<Double> pq:存放树中和非树中顶点之间的有效横切边 |
Prim算法始终将图中的顶点切分成两个集合,最小生成树和非最小生成树顶点,通过不断的重复做某些操作,可以主键将非最小生成树中的顶点加入到最小生成树中,直到所有的顶点,都加入到最小生成树中
在设计API的时候,使用最小索引优先队列存放树中顶点与非树中顶点的有效横切边,那么它是如何表示的呢?我们可以让最小索引处优先队列的索引值表示圆的顶点,让最小索引优先队列中的值表示其他某个顶点到当前顶点的边权重。
初始化状态,线默认0是最小生成树中的唯一顶点,其他的顶点都不再最小生成树中,此时横切边就是顶点0的邻接表中0-2,0-4,0-6,0-7这四条边,我们只需要将索引优先队列的2、4、6、7索引处分别存储这些边的权重值就可以表示了
现在只需要从这四条横切边中找出权重最小的边,如何把对应的顶点加进 最小生成树 中即可。
所以找到0-7这条横切边的权重最小,因此0-7这条边添加进来,此时0和7属于最小生成树的顶点。其他的不属于,现在顶点7的邻接表中的边也成为了横切边,这时需要做两个操作:
package com.renecdemo.prim;
import com.renecdemo.graph.Queue;
import com.renecdemo.queue.IndexMinPriorityQueue;
import com.renecdemo.weighted.Edge;
import com.renecdemo.weighted.EdgeWeightedGraph;
// Prim算法实现最小生成树
public class PrimMST {
// 索引代表顶点,值表示当前顶点和最小生成树之间的最短边
private Edge[] edgeTo;
// 索引代表顶点,值表示当前顶点和最小生成树之间的最短边的权重
private double[] distTo;
// 索引代表顶点,如果当前顶点已经在树中,则值为true,否则为false
private boolean[] marked;
// 存放树中和非树中顶点之间的有效横切边
private IndexMinPriorityQueue<Double> pq;
// 根据一副加权无向图,创建最小生成树计算对象
public PrimMST(EdgeWeightedGraph G) {
// 初始化变量
this.edgeTo = new Edge[G.V()];
this.distTo = new double[G.V()];
// 初始化权重
for (int i = 0; i < distTo.length; i++) {
// 让double数组中的每个值都等于Double类型所能存储的最大值
// 当权重默认为无限大时,意味着该边永远不可能是最短边
distTo[i] = Double.POSITIVE_INFINITY;
}
this.marked = new boolean[G.V()];
this.pq = new IndexMinPriorityQueue<Double>(G.V());//16
// 默认让顶点0进入到树中,当树中只有一个顶点0
distTo[0] = 0.0;// 顶点0 的权重为 0.0
pq.insert(0,0.0);// 它的横切边为 0, 该边权重为0.0
// 遍历所以最小优先队列
while (!pq.isEmpty()){
// 切分 - 对 顶点0 进行切分
visit(G,pq.delMin());
}
}
// 将顶点v添加到最小生成树中,并且更新数据
private void visit(EdgeWeightedGraph G,int v){
// 把顶点v添加到最小生成树中
marked[v] = true;// 标识 顶点v 在树中已存在
/*
遍历 G图 中 顶点v 的邻接表
*/
for (Edge e : G.adj(v)) {
int w = e.other(v);// 获得v顶点外的另一个顶点
/*
判断 w 顶点是否在树中,如果在,那么进行下一次循环
- 如果 w顶点 在树中就无需为w顶点添加到树中了
*/
if (marked[w]){
continue;
}
/*
判断边 e边 的权重是否小于从w到树中已经存在最小边的权重
- 小于才做更新操作
*/
if (e.weight()<distTo[w]){
// 更新数据;更新记录的权重和最短边
edgeTo[w] = e;
distTo[w] = e.weight();
// 更改优先队列中顶点的权重
if (pq.contains(w)){
pq.changeItem(w,e.weight());
}else {
pq.insert(w,e.weight());
}
}
}
}
// 获取最小生成树的所有边
public Queue<Edge> edges(){
// 创建队列edges
Queue<Edge> allEdge = new Queue<>();
for (int i = 0; i < edgeTo.length; i++) {
if (edgeTo[i] != null){
allEdge.enqueue(edgeTo[i]);
}
}
return allEdge;
}
}
Kruskal算法师计算一副加权无向图的最小生成树的另外一种算法,它的主要思想是按照边的权重(从小到大)处理它们,及那个边加入最小生成树中,加入的边不会与已经加入最小生成树的边构成环,直到树中含有V-1条边为止
Pirm算法 是一条边一条边的构造最小生成树,每一步都为一棵树添加一条边。
kruskal算法 构造最小生成树的时候也是一条边一条边的构造,但它的切分规则不一样。
类名 | KruskalMST |
---|---|
构造方法 | KruskalMST(EdgeWeightedGraph G):根据一副加权无向图,创建最小生成树计算对象 |
成员方法 | 1. public Queue<Edge> edges():获取最小生成树的所有边 |
成员变量 | 1. private Queue<Edge> mst:保存最小生成树的所有边2. private UF_Tree_Weighted uf:索引代表顶点,使用connect方法可以判断两个顶点是否在同一颗树种,使用union方法可以将两个顶点所在的树合并3. private MinPriorityQueue<Edge> pq:存储图中所有的边,使用最小优先队列,对边按照权重进行排序 |
在设计API的时候,使用了MinpriorityQueue<Edge> pq存储图中所有的边,每次使用pq.delMin()取出权重最小的边,并得到该边关联的两个顶点v和w,通过uf.connect(v,w)判断v和w是否已经连通。
package com.renecdemo.graph;
import com.renecdemo.queue.MinPriorityQueue;
import com.renecdemo.weighted.Edge;
import com.renecdemo.weighted.EdgeWeightedGraph;
import com.renexdemo.tree.UF_Tree_Weighted;
/**
* Kruskal 算法实现
*/
public class KruskalMST {
// 保存最小生成树的所有边
private Queue<Edge> mst;
/**
* 索引代表顶点,
* 使用connect方法可以判断两个顶点是否在同一颗树中,
* 使用union方法可以将两个顶点所在的树合并
*/
private UF_Tree_Weighted uf;
// 存储图中所有的边,使用最小优先队列,对边按照权重进行排序
private MinPriorityQueue<Edge> pq;
/**
* 初始化构造
* @param G
*/
public KruskalMST(EdgeWeightedGraph G){
// 初始化
this.mst = new Queue<>();
this.uf = new UF_Tree_Weighted(G.V());
this.pq = new MinPriorityQueue<>(G.E()+1);
// 把图中所有的边添加到pq中
for (Edge e : G.edges()) {
pq.insert(e);
}
/*
pq数组不为空
并且
最小生成树的所有边不应该小于子图中顶点数-1
- 如果所有边小于 子图中顶点数-1 那么代表最小生成树已经规划完毕
*/
while (!pq.isEmpty() && mst.size() < G.V()-1){
// 找到权重最小的边
Edge e = pq.delMin();
// 找到该边的两个顶点
int v = e.either();
int w = e.other(v);
// 判断这两个顶点是否已经在同一棵树中
if (uf.connected(v,w)){
// 在一棵数中不做处理直接跳过当前循环
continue;
}
// 不再一棵树中,那么合并
uf.union(v,w);
// 让边e进入到mst队列汇总
mst.enqueue(e);
}
}
/**
* 获取最小生成树的所有边
* @return
*/
public Queue<Edge> edges(){
return mst;
}
}
快来看看这篇好文章吧~~!! 😊👉(全篇详细讲解)ElasticSearch8.7 搭配 SpringDataElasticSearch5.1 的使用