持续学习&持续更新中…
学习态度:脚踏实地
生成树
用最少的边将图的所有顶点连通起来,使该图依旧是一个连通图,我们就称之为生成树(支撑树)
一个连通图的生成树(支撑树)可以有很多颗
最小生成树
一个连通图可以有很多颗生成树
如果某个生成树的所有边的权值之和在众多生成树的所有边的权值之和之中是最小的,那么该生成树就是最小生成树
切分定理
Prim算法
Prim算法是建立在切分定理的基础上的
Prim算法实现
自定义最小堆MinHeap:
public class MinHeap<E> {
private static final int DEFAULT_CAPACITY = 10;
protected int size;
private Comparator<E> comparator;
private E[] elements;
public MinHeap() {
this.elements = (E[]) new Object[DEFAULT_CAPACITY];
}
public MinHeap(Comparator<E> comparator) {
this.comparator = comparator;
this.elements = (E[]) new Object[DEFAULT_CAPACITY];
}
public MinHeap(E[] elements) {
this(elements, null);
}
public MinHeap(Collection<E> elements) {
this(elements, null);
}
public MinHeap(E[] elements, Comparator<E> comparator) {
this.comparator = comparator;
final int elementSize = elements.length;
if (null == elements || elementSize == 0) {
this.elements = (E[]) new Object[DEFAULT_CAPACITY];
} else {
int capacity = elementSize < DEFAULT_CAPACITY ? DEFAULT_CAPACITY : elementSize;
E[] data = (E[]) new Object[capacity];
System.arraycopy(elements, 0, data, 0, elementSize);
this.elements = data;
this.size = elementSize;
heapify();
}
}
public MinHeap(Collection<E> elements, Comparator<E> comparator) {
this.comparator = comparator;
final int elementSize = elements.size();
if (null == elements || elementSize == 0) {
this.elements = (E[]) new Object[DEFAULT_CAPACITY];
} else {
int capacity = elementSize < DEFAULT_CAPACITY ? DEFAULT_CAPACITY : elementSize;
E[] data = (E[]) new Object[capacity];
int i = 0;
for (E e : elements) {
data[i++] = e;
}
this.elements = data;
this.size = elementSize;
heapify();
}
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public void add(E element) {
elementNotNullCheck(element);
ensureCapacity();
elements[size] = element;
siftUp(size);
size++;
}
public void addAll(Collection<E> elements) {
if (null == elements || elements.size() == 0) {
return;
}
for (E e : elements) {
add(e);
}
}
public E remove() {
E root = elements[0];
elements[0] = elements[size - 1];
elements[size - 1] = null;
size--;
siftDown(0);
return root;
}
public E replace(E element) {
// 两个log(n) 不推荐
// E root = remove();
// add(element);
// return root;
elementNotNullCheck(element);
final E root = get();
elements[0] = element;
siftDown(0);
return root;
}
public E get() {
emptyCheck();
return elements[0];
}
public void clear() {
for (int i = 0; i < size; i++) {
elements[i] = null;
}
size = 0;
}
private void heapify() {
// 使用自下而上的siftDown
for (int i = (size >> 1) - 1; i >= 0; i--) {
siftDown(i);
}
}
private void siftUp(int index) {
E element = elements[index];
while (index > 0) { // 保证有父节点
int parentIndex = (index - 1) >> 1;
E parentElement = elements[parentIndex];
int cmp = compare(element, parentElement);
if (cmp <= 0) break;
elements[index] = parentElement;
index = parentIndex;
}
elements[index] = element;
}
private void siftDown(int index) {
/*
- 完全二叉树的非叶子节点个数为:floor( n / 2 )
*/
E element = elements[index];
int notLeafCount = size >> 1; // 非叶子节点个数
while (index < notLeafCount) {
/* 完全二叉树非叶子节点特性:
- 要么拥有左右子节点(度为2)
- 要么只有左子节点(度为1)
*/
int maxChildIndex;
E maxChildElement;
int leftChildIndex = index + index + 1;
int rightChildIndex = leftChildIndex + 1;
E leftChildElement = elements[leftChildIndex];
maxChildIndex = leftChildIndex;
maxChildElement = leftChildElement;
if (rightChildIndex < size) { // 存在右子节点
E rightChildElement = elements[rightChildIndex];
if (compare(rightChildElement, leftChildElement) > 0) {
maxChildIndex = rightChildIndex;
maxChildElement = rightChildElement;
}
}
if (compare(element, maxChildElement) >= 0) break;
elements[index] = maxChildElement;
index = maxChildIndex;
}
elements[index] = element;
}
private int compare(E e1, E e2) {
if (comparator != null) {
return comparator.compare(e2, e1);
}
return ((Comparable<E>) e2).compareTo(e1);
}
private void elementNotNullCheck(E element) {
if (null == element) throw new IllegalArgumentException("Element must not be null!");
}
private void emptyCheck() {
if (size == 0) throw new IndexOutOfBoundsException("This heap is empty!");
}
private void ensureCapacity() {
if (size == elements.length) {
int newCapacity = elements.length + (elements.length >> 1);
E[] newElements = (E[]) new Object[newCapacity];
for (int i = 0; i < size; i++) {
newElements[i] = elements[i];
}
elements = newElements;
}
}
}
重构接口Graph:
public abstract class Graph<V, E> {
protected WeightManager weightManager;
// 无权图
public Graph() {
}
// 有权图
public Graph(WeightManager<E> weightManager) {
this.weightManager = weightManager;
}
// 顶点数量
public abstract int vertexSize();
// 边的数量
public abstract int edgeSize();
public abstract void addVertex(V v);
public abstract void removeVertex(V v);
public abstract void addEdge(V from, V to);
public abstract void addEdge(V from, V to, E weight);
public abstract void removeEdge(V from, V to);
// 广度优先遍历
public abstract void bfs(V begin, Visitor<V> visitor);
// 深度优先遍历
public abstract void dfs(V begin, Visitor<V> visitor);
// 对DAG(有向无环图)进行拓扑排序
public abstract List<V> topologicalSort();
// 连通图的最小生成树
public abstract Set<EdgeInfo<V, E>> mst();
public interface Visitor<V> {
boolean vertex(V v); // 返回true,就终止遍历
}
public static class EdgeInfo<V, E> {
private V from;
private V to;
private E weight;
protected EdgeInfo(V from, V to, E weight) {
this.from = from;
this.to = to;
this.weight = weight;
}
public V getFrom() {
return from;
}
public void setFrom(V from) {
this.from = from;
}
public V getTo() {
return to;
}
public void setTo(V to) {
this.to = to;
}
public E getWeight() {
return weight;
}
public void setWeight(E weight) {
this.weight = weight;
}
@Override
public String toString() {
return "EdgeInfo{" +
"from=" + from +
", to=" + to +
", weight=" + weight +
'}';
}
}
// 对有权图的权值进行管理
public interface WeightManager<E> {
int compare(E weight1, E weight2); // 比较权值的大小
E add(E weight1, E weight2); // 为最短路径服务(两个权值相加)
}
}
使用Prim算法实现最小生成树:
public class ListGraph<V, E> extends Graph<V, E> {
// ...
private final Comparator<Edge<V, E>> edgeComparator
= (e1, e2) -> weightManager.compare(e1.weight, e2.weight);
public ListGraph() {
}
public ListGraph(WeightManager<E> weightManager) {
super(weightManager);
}
// ...
// 边
private static class Edge<V, E> {
E weight; // 边的权值
Vertex<V, E> from; // 这条边从哪个顶点出发
Vertex<V, E> to; // 这条边要到达哪个顶点
Edge(Vertex<V, E> from, Vertex<V, E> to, E weight) {
this.from = from;
this.to = to;
this.weight = weight;
}
@Override
public boolean equals(Object o) {
Edge<V, E> edge = (Edge<V, E>) o;
return from.equals(edge.from) && to.equals(edge.to);
}
@Override
public int hashCode() {
int result = 0;
result = 31 * result + from.hashCode();
result = 31 * result + to.hashCode();
return result;
}
public EdgeInfo<V, E> edgeInfo() {
return new EdgeInfo<>(from.value, to.value, weight);
}
@Override
public String toString() {
return "Edge{" +
"from=" + from +
", to=" + to +
", weight=" + weight +
'}';
}
}
// minimum spanning tree
@Override
public Set<EdgeInfo<V, E>> mst() {
return prim();
}
private Set<EdgeInfo<V, E>> prim() {
final Iterator<Vertex<V, E>> iterator = vertices.values().iterator();
if (!iterator.hasNext()) return null; // 如果没有顶点的话(空图)返回null
// 相当于PPT上的A集合:最小生成树的边集
final Set<EdgeInfo<V, E>> edgeInfos = new HashSet<>();
// 相当于PPT上的S集合:最小生成树的顶点
final Set<Vertex<V, E>> addedVertex = new HashSet<>();
final Vertex<V, E> startVertex = iterator.next();
addedVertex.add(startVertex);
final MinHeap<Edge<V, E>> minHeap = new MinHeap<>(startVertex.outEdges, edgeComparator);
// final int edgeSize = vertices.size() - 1; // 终止条件: 边集的数量 == 顶点的数量 - 1
// while (!minHeap.isEmpty() && edgeInfos.size() < edgeSize) {
final int verticesSize = vertices.size(); // 终止条件:最小生成树的顶点的数量 == 顶点的数量
while (!minHeap.isEmpty() && addedVertex.size() < verticesSize) {
Edge<V, E> edge = minHeap.remove();
if (addedVertex.contains(edge.to)) continue;
edgeInfos.add(edge.edgeInfo());
addedVertex.add(edge.to);
minHeap.addAll(edge.to.outEdges);
}
return edgeInfos;
}
}
测试:
private static final Graph.WeightManager<Double> weightManager = new Graph.WeightManager<Double>() {
@Override
public int compare(Double weight1, Double weight2) {
return weight1.compareTo(weight2);
}
@Override
public Double add(Double weight1, Double weight2) {
return weight1 + weight2;
}
};
/**
* 有向图(带权值管理器)
*/
private static Graph<Object, Double> directedGraphWithWM(Object[][] data) {
Graph<Object, Double> graph = new ListGraph<>(weightManager);
for (Object[] edge : data) {
if (edge.length == 1) {
graph.addVertex(edge[0]);
} else if (edge.length == 2) {
graph.addEdge(edge[0], edge[1]);
} else if (edge.length == 3) {
double weight = Double.parseDouble(edge[2].toString());
graph.addEdge(edge[0], edge[1], weight);
}
}
return graph;
}
/**
* 无向图(带权值管理器)
*/
private static Graph<Object, Double> undirectedGraphWithWM(Object[][] data) {
Graph<Object, Double> graph = new ListGraph<>(weightManager);
for (Object[] edge : data) {
if (edge.length == 1) {
graph.addVertex(edge[0]);
} else if (edge.length == 2) {
graph.addEdge(edge[0], edge[1]);
graph.addEdge(edge[1], edge[0]);
} else if (edge.length == 3) {
double weight = Double.parseDouble(edge[2].toString());
graph.addEdge(edge[0], edge[1], weight);
graph.addEdge(edge[1], edge[0], weight);
}
}
return graph;
}
public static final Object[][] MST_01 = {
{0, 2, 2}, {0, 4, 7},
{1, 2, 3}, {1, 5, 1}, {1, 6, 7},
{2, 4, 4}, {2, 5, 3}, {2, 6, 6},
{3, 7, 9},
{4, 6, 8},
{5, 6, 4}, {5, 7, 5}
};
public static final Object[][] MST_02 = {
{"A", "B", 17}, {"A", "F", 1}, {"A", "E", 16},
{"B", "C", 6}, {"B", "D", 5}, {"B", "F", 11},
{"C", "D", 10},
{"D", "E", 4}, {"D", "F", 14},
{"E", "F", 33}
};
public static void main(String[] args) {
// Graph<Object, Double> graph = undirectedGraphWithWM(Data.MST_01);
Graph<Object, Double> graph = undirectedGraphWithWM(Data.MST_02);
final Set<Graph.EdgeInfo<Object, Double>> edgeInfos = graph.mst();
for (Graph.EdgeInfo<Object, Double> edgeInfo : edgeInfos) {
System.out.println(edgeInfo);
}
}
Kruskal算法
生成树:使用最少的边连通该图,使该图依然是一个连通图;V = A + 1;顶点的数量 = 生成树的边的数量 + 1
比如:如果该图有3个顶点,那么只需2条边即可组成一颗生成树;如果该图有3条边的话,那么它就不会是一颗生成树;并且如果该图有3条边的话,该图是会有环的。
因此,有环图一定不会是一颗生成树。
Kruskal算法实现
自定义并查集UnionFind:
// QuickUnion + 基于size + PathHalving
// 实现并查集(UnionFind)
public class UnionFind<V> {
private final Map<V, Node<V>> nodes = new HashMap<>();
// 初始化每一个元素V为一个单独的集合
public void makeSet(V v) {
if (nodes.containsKey(v)) return;
nodes.put(v, new Node<>(v));
}
private static class Node<V> {
V value;
Node<V> parent;
int size; // 基于size的优化
Node(V value) {
this.value = value;
parent = this; // 初始化时,每一个元素的父节点都是自己
size = 1; // 初始化时,每一个元素单独成为一颗树,树的元素个数肯定为1
}
}
private Node<V> findRootNode(V v) {
Node<V> vNode = nodes.get(v);
if (vNode == null) return null;
while (!Objects.equals(vNode.value, vNode.parent.value)) {
vNode.parent = vNode.parent.parent;
vNode = vNode.parent;
}
return vNode;
}
public V find(V v) {
final Node<V> rootNode = findRootNode(v);
return rootNode == null ? null : rootNode.value;
}
public void union(V v1, V v2) {
final Node<V> v1RootNode = findRootNode(v1);
final Node<V> v2RootNode = findRootNode(v2);
if (v1RootNode == null || v2RootNode == null) return;
if (Objects.equals(v1RootNode.value, v2RootNode.value)) return;
if (v1RootNode.size < v2RootNode.size) {
v1RootNode.parent = v2RootNode;
v2RootNode.size += v1RootNode.size;
} else {
v2RootNode.parent = v1RootNode;
v1RootNode.size += v2RootNode.size;
}
}
public boolean isSame(V v1, V v2) {
return Objects.equals(find(v1), find(v2));
}
}
Kruskal算法实现:(使用UnionFind巧妙地判断生成树是否有环)
// minimum spanning tree
@Override
public Set<EdgeInfo<V, E>> mst() {
return kruskal();
}
private Set<EdgeInfo<V, E>> kruskal() {
int verticesSize = vertices.size() - 1; // 终止条件:生成树的边集的数量 == 顶点集的数量 - 1
if (-1 == verticesSize) return null; // 0-1 == -1,如果没有顶点的话返回null
final Set<EdgeInfo<V, E>> edgeInfos = new HashSet<>(); // 返回给用户的生成树的边的信息
// 使用并查集巧妙的判断是否有环
final UnionFind<Vertex<V, E>> unionFind = new UnionFind<>();
// 初始化并查集(初始化时,每一个顶点都是一个单独的集合)
vertices.values().forEach(unionFind::makeSet);
// 使用最小堆辅助我们方便快速的找出最小权值的边
final MinHeap<Edge<V, E>> minHeap = new MinHeap<>(edges, edgeComparator);
while (!minHeap.isEmpty() && edgeInfos.size() < verticesSize) {
Edge<V, E> edge = minHeap.remove();
// 如果某个边的起点和终点属于同一个集合,那么这条边就不能组成生成树
// 因为如果让这条边组成生成树的话,那么这颗生成树就有环了
if (unionFind.isSame(edge.from, edge.to)) continue;
edgeInfos.add(edge.edgeInfo());
// 将这条边的起点和终点这两个顶点所在的集合合并为一个集合
unionFind.union(edge.from, edge.to);
}
return edgeInfos;
}
注意
- Java的堆是PriorityQueue这个类,默认是一个小顶堆(最小堆)
- 使用PriorityQueue时,批量建堆的同时不能传比较器
- 使用PriorityQueue时,传比较器的同时不能批量建堆
- 因此代码实现最小生成树时,使用了自己实现的最小堆
public static void main(String[] args) {
// 最小堆
PriorityQueue<Integer> heap = new PriorityQueue<>();
// PriorityQueue<Integer> heap = new PriorityQueue<>(new ArrayList<>());
// PriorityQueue<Integer> heap = new PriorityQueue<>(new Comparator<Integer>() {
// @Override
// public int compare(Integer o1, Integer o2) {
// return 0;
// }
// });
for (int i = 0; i < 10; i++) {
heap.offer(1 + i);
}
System.out.println(heap.peek());
}
参考
小码哥李明杰老师课程: 恋上数据结构与算法 第二季.
本文完,感谢您的关注支持!