生成树: 图的生成树就是一颗含有其所有顶点的无环连通子图。
最小生成树: 即给定一个加权无向图,找到它的一颗最小生成树(权值最小)。
因此最小生成树的研究范围是:
带权无向图,且是连通图。
如下给出加权无向图的数据结构:
带权重的边:
/**
* 带权重的边
* @author MaoLin Wang
* @date 2020/2/239:49
*/
public class Edge implements Comparable<Edge>{
private final int v;
private final int w;
private final double weight;
public Edge(int v, int w, double weight) {
if (v < 0) throw new IllegalArgumentException("vertex index must be a nonnegative integer");
if (w < 0) throw new IllegalArgumentException("vertex index must be a nonnegative integer");
if (Double.isNaN(weight)) throw new IllegalArgumentException("权重必须为数值");
this.v = v;
this.w = w;
this.weight = weight;
}
/**
* 返回权重
* @return
*/
public double weight() {
return weight;
}
/**
* 返回边两端的顶点之一
* @return
*/
public int either() {
return v;
}
/**
* 返回另一个顶点
* @param vertex
* @return
*/
public int other(int vertex) {
if (vertex == v) return w;
else if (vertex == w) return v;
else throw new IllegalArgumentException("Illegal endpoint");
}
@Override
public int compareTo(Edge edge) {
return Double.compare(this.weight, edge.weight);
}
public String toString() {
return String.format("%d-%d %.5f", v, w, weight);
}
}
加权无向图:
同之前将的几个图基本一样,只是这是添加边是通过Edge对象
还要注意的是,在返回所有的边时,要考虑自环的情况。
/**
* 加权无向图
* @author MaoLin Wang
* @date 2020/2/239:54
*/
public class EdgeWeightedGraph {
private static final String NEWLINE = System.getProperty("line.separator");
private final int V;
private int E;
private Bag<Edge>[] adj;
public EdgeWeightedGraph(int V) {
if (V < 0) throw new IllegalArgumentException("Number of vertices must be nonnegative");
this.V = V;
this.E = 0;
adj = (Bag<Edge>[]) new Bag[V];
for (int v = 0; v < V; v++) {
adj[v] = new Bag<Edge>();
}
}
//添加一条边
private void addEdge(Edge e) {
int v = e.either();
int w = e.other(v);
validateVertex(v);
validateVertex(w);
adj[v].add(e);
adj[w].add(e);
E++;
}
//返回顶点v的邻边
public Iterable<Edge> adj(int v) {
validateVertex(v);
return adj[v];
}
//顶点v的度
public int degree(int v) {
validateVertex(v);
return adj[v].size();
}
//返回所有边
public Iterable<Edge> edges() {
Bag<Edge> list = new Bag<Edge>();
for (int v = 0; v < V; v++) {
int selfLoops = 0;
for (Edge e : adj(v)) {
if (e.other(v) > v) {
list.add(e);
}
else if (e.other(v) == v) { //是自环 如 1-1
if (selfLoops % 2 == 0) list.add(e);//对于自环,只需要添加一个副本即可,因为我们在addEdge时,添加了两条一样的边
selfLoops++;
}
}
}
return list;
}
private void validateVertex(int v) {
if (v < 0 || v >= V)
throw new IllegalArgumentException("vertex " + v + " is not between 0 and " + (V-1));
}
public int V() {
return V;
}
public int E() {
return E;
}
@Override
public String toString() {
return "顶点:" + V +
", 边=" + E +
"\n" + Arrays.toString(adj);
}
}
一、Prim算法
Prim算法的每一步都会为一颗正在生长的树添加一条边。树是从一个顶点开始的,然后每次添加一条边,该边是下一条 连接树中的顶点和不在树中的顶点且权重最小的边。
该算法能够得到任意加权连通图的最小生成树。
在这之前要讲一下切分定理:
图的一种切分就是将图的所有顶点分为两个非空的且不重叠的集合。
横切边是一条连接两个属于不同集合的顶点的边(一边连接这个集合,一边接连另一个集合)。
切分定理就是给定任意切分,横切边中权值最小的边必然属于最小生成树;
Prim的延时实现
1.1数据结构
我们需要如下的数据结构来实现Prim算法,其中BinaryHeap
是前面讲过的优先队列(小顶堆实现),需要的可以看这篇文章:优先队列(堆)以及堆排序-------java实现
/**
* 最小生成树的顶点
*/
private boolean[] marked;
/**
* 最小生成树的边
*/
private Queue<Edge> mst;
/**
* 横切边(包括失效的边)
*/
private BinaryHeap<Edge> pq;
1.2 实现思路
1.选择一个顶点标记它,然后将连接这个顶点和其他所有不再树中的顶点的边加入优先队列中(优先队列按边的权重从小到大排列);
2.从优先队列中取出权重最小的边,如果边的两个顶点都被访问过了,则代表该边失效了,继续取下一个最小的边;
3.如果权重最小的边(即横切边)没有失效,则将其加入最小生成树队列,将对该边未被访问的点再次进行 步骤1的操作,直到优先队列为空,此时最小生成树的边都在mst队列中了。
如下图:
选取0作为开始的顶点,然后将与0相连的且其他不在树中的边加入优先队列(后边直接展示优先队列状态):
*表示新加入的边,△表示失效的边,图中红色的边为加入最小生成树中的边,蓝色的边未优先队列中的边;橙色的节点表示最小生成树,绿色的表示未加入
边 权重
* 0-7 0.16
* 0-2 0.26
* 0-4 0.38
* 6-0 0.58
将权重最小的边0-7从优先队列取出,并加入最小生成树,继续对未被访问的顶点7进行上述操作:
* 1-7 0.19
0-2 0.26
* 5-7 0.28
* 2-7 0.34
* 4-7 0.37
0-4 0.38
6-0 0.58
将边 1-7取出加入生成树:
0-2 0.26
5-7 0.28
* 1-3 0.29
* 1-5 0.32
2-7 0.34
* 1-2 0.36
4-7 0.37
0-4 0.38
6-0 0.58
将边0-2取出加入生成树:
* 2-3 0.17
5-7 0.28
1-3 0.29
1-5 0.32
△ 2-7 0.34
△ 1-2 0.36
4-7 0.37
0-4 0.38
* 6-2 0.40
6-0 0.58
将2-3取出加入生成树:
5-7 0.28
△ 1-3 0.29
1-5 0.32
△ 2-7 0.34
△ 1-2 0.36
4-7 0.37
0-4 0.38
6-2 0.40
* 3-6 0.52
6-0 0.58
取出5-7加入生成树:
△ 1-3 0.29
△ 1-5 0.32
△ 2-7 0.34
* 5-4 0.35
△ 1-2 0.36
4-7 0.37
0-4 0.38
6-2 0.40
3-6 0.52
6-0 0.58
此时前面三个边已经失效了,全部移除,将边5-4加入生成树:
△ 1-2 0.36
△ 4-7 0.37
△ 0-4 0.38
6-2 0.40
3-6 0.52
6-0 0.58
6-4 0.93
将未失效的6-2加入生成树:
此时优先队列剩余边全部失效,mst队列已经得到了该图从0开始的最小生成树。
1.3 Prim的延时实现
public class LazyPrimMST {
/**
* 最小生成树的顶点
*/
private boolean[] marked;
/**
* 最小生成树的边
*/
private Queue<Edge> mst;
/**
* 横切边(包括失效的边)
*/
private BinaryHeap<Edge> pq;
public LazyPrimMST(EdgeWeightedGraph G){
pq=new BinaryHeap<>();
marked=new boolean[G.V()];
mst=new Queue<>();
//假设G是连通的
visit(G,0);
while (!pq.isEmpty()){
//从pq中得到权重最小的边
Edge edge = pq.deleteMin();
int v=edge.either(),w=edge.other(v);
if (marked[v]&&marked[w]){//已失效,忽略
continue;
}
//当前边加入最小生成树
mst.enqueue(edge);
//访问当前横切边未被访问的那个顶点v或w
if (!marked[v]){
visit(G,v);
}
if (!marked[w]){
visit(G,w);
}
}
}
/**
* 将所有与v连接且未被标记的顶点的边加入横切边
* @param G
* @param v
*/
private void visit(EdgeWeightedGraph G, int v) {
marked[v]=true;
for (Edge edge:G.adj(v)){
if (!marked[edge.other(v)]){
pq.insert(edge);
}
}
}
/**
* 返回最小生成树的所有边
* @return
*/
public Iterable<Edge> edges(){
return mst;
}
/**
* 最小生成树的权重和
* @return
*/
public double weight(){
double weight=0;
for (Edge edge:mst){
weight+=edge.weight();
}
return weight;
}
}
Prim算法的延时实现(V个顶点E条边)所需空间与E成正比,所需时间与ElogE成正比(最坏情况)
1.4 Prim算法的即时实现
对于上面的延时实现,我们发现优先队列中存在大量失效的边,如果我们可以将失效的边从中删除就好了。
对于每个非树顶点w,我们不需要保存所有从w到树顶点的边,而只要保存权重最小的那个边就可以了,添加顶点v到树中后,就需要更新权重最小的边,因为可能 v-w 的权重比之前w到另外一个树顶点的边更小。而对于最小权重之外的边,其早晚会失效。
在即时实现中,我们使用edgeTo[v
]保存v和树最近的边,用distTo[v]
保存该边的权重,distTo[v]
应始终保持v到树顶点的最小权重值。
另外使用索引优先队列将顶点v作为索引,顶点v到树顶点最小的边权重distTo[v]作为索引关联的值(索引优先队列即按该权重进行排序)
(索引优先队列参考 算法4》索引优先队列笔记-----java实现一文)
如下:
public class PrimMST {
/**
* edgeTo[v] 顶点v距离树最近的边
*/
private Edge[] edgeTo;
/**
* distTo[v] 保存顶点v到树顶点最小边的权重
*/
private double[] distTo;
private boolean[] marked;
private double weight=0.0;
/**
* 有效的横切边:索引优先队列,顶点v作为索引,disTo[v]作为索引关联的值
*/
private IndexMinPQ<Double>pq;
public PrimMST(EdgeWeightedGraph G){
edgeTo=new Edge[G.V()];
distTo=new double[G.V()];
marked=new boolean[G.V()];
for (int v = 0; v < G.V(); v++) {
//初始化为无穷大
distTo[v]=Double.POSITIVE_INFINITY;
}
pq=new IndexMinPQ<>(G.V());
//如果是连通的,则直接走prim(),这个循环不需要加
for (int v = 0; v < G.V(); v++) {
if (!marked[v]){
prim(G,v);
}
}
}
}
举例说明(仍使用上面的图):
如下图,以0为起点,此时我们将连接0和非树节点的边加入索引优先队列,edgeTo
和distTo
的状态为:
edgeTo[] distTo[]
2 0-2 0.26
4 0-4 0.38
6 0-6 0.58
7 0-7 0.16
索引优先队列为:
index Key
7 0.16
2 0.26
4 0.38
6 0.58
接着从索引优先队列取出第一个索引7,然后更新edgeTo和distTo:
edgeTo[] distTo[]
1 7-1 0.19
2 0-2 0.26 因为边7-2的权重大于边0-2的0.26 因此不需要更新索引优先队列中索引为2的关联的对象
4 7-4 0.37 因为边7-4的权重小于边0-4的0.38 因此需要更新索引优先队列中索引为4的关联的对象
5 7-5 0.28
6 0-6 0.58
7 0-7 0.16
队列中的状态:
index Key
1 0.19
2 0.26 //新的权重()比0.26大,所以不做更新
4 0.37 //由原来的0.38更新为较小的0.37
5 0.28
6 0.58
到此就可以看到,索引优先队列中保存的永远是顶点v距离树最近的边(每加入一个顶点到树中就做更新),而不像上面延时版本一样保存了所有的边,这样直到队列为空,便可以得到一颗最小生成树。
实现如下:
public class PrimMST {
/**
* edgeTo[v] 顶点v距离树最近的边
*/
private Edge[] edgeTo;
/**
* distTo[v] 保存顶点v到树顶点最小边的权重
*/
private double[] distTo;
private boolean[] marked;
/**
* 最小生成树的权重
*/
private double weight=0.0;
/**
* 有效的横切边:索引优先队列,顶点v作为索引,disTo[v]作为索引关联的值
*/
private IndexMinPQ<Double>pq;
public PrimMST(EdgeWeightedGraph G){
edgeTo=new Edge[G.V()];
distTo=new double[G.V()];
marked=new boolean[G.V()];
for (int v = 0; v < G.V(); v++) {
//初始化为无穷大
distTo[v]=Double.POSITIVE_INFINITY;
}
pq=new IndexMinPQ<>(G.V());
//如果是连通的,则直接走prim(),这个循环不需要加
for (int v = 0; v < G.V(); v++) {
if (!marked[v]){
prim(G,v);
}
}
check(G);
}
private void prim(EdgeWeightedGraph G,int v) {
distTo[v]=0.0;
//将当前顶点作为索引,当前顶点距离树权重最小的边的权重作为关联的key加入索引优先队列
pq.insert(v,distTo[v]);
while (!pq.isEmpty()){
//取出权重最小的索引(顶点)
int w = pq.delMin();
visit(G,w);
}
}
private void visit(EdgeWeightedGraph G, int v) {
marked[v]=true;
weight+=distTo[v];
for (Edge e:G.adj(v)){
int w=e.other(v);
if (marked[w]){
continue;
}
if (e.weight()<distTo[w]){//只有当新的边的权重小于之前的distTo[w]值的时候,才更新值
edgeTo[w]=e;
distTo[w]=e.weight();
if (pq.contains(w)){ //索引优先队列有这个索引,更新对应的权重值
pq.changeKey(w,distTo[w]);
}else {//否则插入新的
pq.insert(w,distTo[w]);
}
}
}
}
public Iterable<Edge> edges(){
Queue<Edge> mst = new Queue<Edge>();
for (int v = 0; v < edgeTo.length; v++) {
Edge e = edgeTo[v];
if (e != null) {
mst.enqueue(e);
}
}
return mst;
}
public double weight(){
return weight;
}
}
二、Kruskal法
Kruskal算法的思想是:按照边的权重从小到大的顺序处理它们,每次将权重最小的边加入树中,新加入的边不会与已经加入的边构成环,直到树中有V-1条边为止。是从边出发。
该算法的实现有两个关键:
1. 需要将所有边按权重大小排序,这样每次才能取出权重最小的一条边,实现此需要用到前面的优先队列(小顶堆)
2. 为了避免形成环,我们需要借助前面讲过的并查集判断树加入新取出的边后是否有环,即如果该边的两个顶点属于同一个属性集,则会形成环;否则将该边加入生成树,并将边的两个顶点连接
依然以上面的图为例:
一开始每个顶点都属于单独的分量
1.先选择权重最小的边 0-7,将0和7两个顶点使用union(0,7)连接,此时0和7在一个分量中
2.选取下一个最小边2-3,将2-3加入生成树,并用union连接,此时2和3在一个分量中
3.选取1-7加入生成树,连接1和7,此时1加入7所在的分量
3.选取0-2加入生成树,连接0和2,此时2和3加入0所在分量
4.这时边 1-3 , 1-2 和 7-2 失效,因为这任意一条边连接都构成了环(任意一条边两个顶点都在同一个分量中),只能选取下一个权重最小边5-7
然后是5-4和2-6,最终得到一个最小生成树:
实现如下:
关于并查集的实现可以看前面的文章:《算法第四版》—union-find并查集笔记
public class KruskalMST {
/**
* 最小生成树权重
*/
private double weight=0.0;
/**
* 最小生成树所有边
*/
private Queue<Edge> mst;
public KruskalMST(EdgeWeightedGraph G){
mst=new Queue<>();
//创建一个优先队列
BinaryHeap<Edge> pq= new BinaryHeap<>();
for (Edge e: G.edges()){
//将所有边加入队列中,按权重大小从小到大排序
pq.insert(e);
}
//创建一个并查集
ComPathWeightedQuickUnionFind uf=new ComPathWeightedQuickUnionFind(G.V());
//队列非空且边的个数小于顶点数-1时循环
while (!pq.isEmpty() && mst.size()<G.V()-1){
//得到权重最小的边
Edge edge = pq.deleteMin();
int v=edge.either(),w=edge.other(v);
if (uf.connected(v,w)){
//忽略失效的边
continue;
}
uf.union(v,w);//合并分量
mst.enqueue(edge);//加入生成树
weight+=edge.weight();
}
}
public Iterable<Edge> edges(){
return mst;
}
public double weight(){
return weight;
}
}