最小生成树
点击这里,前提知晓...
一、相关概念
1). 最小生成树
- 最小生成树:在一个完全联通的 【有权】【无向图】 中(联通分量为1),找到一个树结构,v个节点,v-1条边,这个数结构的所有边的权之和最小!
- 应用举例:
- 电缆的布线问题
- 网络设计
- 电路设计
- 注意:针对带权无向图、联通图
2). 切分定理!
- 切分
切分.png
- 横切边
横切边.png
- 切分定理
切分定理.png
- 切分定理的通用性
切分定理通用型.png
二、Lazy Prim算法思想实现最小生成树
1). 算法思想描述
以一个起始点开始进行切分,找对应相邻边并且是另一个切分点的权最小的边以及节点,并将其加入自己的切分点中,然后这两个点位一个切分点,找这个切分点相邻的并且是另一个切分点权最小的边以及节点,并加入自己的切分点中,如此反复,直到将所有点都加入了自己的切分点,算法过程成,最小生成树找到!
2). Lazy Prim 实现
- 辅助数据结构类-最小堆
/**
* @author Liucheng
* @since 2019-10-16
*/
public class MinHeap- {
protected Item[] data;
protected int count;
protected int capacity;
/**
* 构造函数, 构造一个空堆, 可容纳capacity个元素
*/
public MinHeap(int capacity){
data = (Item[])new Comparable[capacity+1];
count = 0;
this.capacity = capacity;
}
/**
* 构造函数, 通过一个给定数组创建一个最小堆
* 该构造堆的过程, 时间复杂度为O(n)
*/
public MinHeap(Item arr[]){
int n = arr.length;
data = (Item[])new Comparable[n+1];
capacity = n;
System.arraycopy(arr, 0, data, 1, n);
count = n;
for(int i = count / 2 ; i >= 1 ; i -- ) {
shiftDown(i);
}
}
/**
* 返回堆中的元素个数
*/
public int size(){ return count; }
/**
* 返回一个布尔值, 表示堆中是否为空
*/
public boolean isEmpty(){ return count == 0; }
/**
* 向最小堆中插入一个新的元素 item
*/
public void insert(Item item){
assert count + 1 <= capacity;
data[count+1] = item;
count ++;
shiftUp(count);
}
/**
* 从最小堆中取出堆顶元素, 即堆中所存储的最小数据
*/
public Item extractMin(){
assert count > 0;
Item ret = data[1];
swap(1 , count);
count --;
shiftDown(1);
return ret;
}
/**
* 获取最小堆中的堆顶元素
*/
public Item getMin(){
assert( count > 0 );
return data[1];
}
/**
* 交换堆中索引为i和j的两个元素
*/
private void swap(int i, int j){
Item t = data[i];
data[i] = data[j];
data[j] = t;
}
//********************
//* 最小堆核心辅助函数
//********************
private void shiftUp(int k){
while( k > 1 && data[k / 2].compareTo(data[k]) > 0 ){
swap(k, k / 2);
k /= 2;
}
}
//********************
//* 最小堆核心辅助函数
//********************
private void shiftDown(int k){
while(2 * k <= count){
// 在此轮循环中,data[k]和data[j]交换位置
int j = 2 * k;
if(j + 1 <= count && data[j+1].compareTo(data[j]) < 0 ) {
j ++;
}
// data[j] 是 data[2*k]和data[2*k+1]中的最小值
if( data[k].compareTo(data[j]) <= 0 ) {
break;
}
swap(k, j);
k = j;
}
}
// 测试 MinHeap
public static void main(String[] args) {
MinHeap
minHeap = new MinHeap(100);
int N = 100; // 堆中元素个数
int M = 100; // 堆中元素取值范围[0, M)
for( int i = 0 ; i < N ; i ++ )
minHeap.insert( new Integer((int)(Math.random() * M)) );
Integer[] arr = new Integer[N];
// 将minheap中的数据逐渐使用extractMin取出来
// 取出来的顺序应该是按照从小到大的顺序取出来的
for( int i = 0 ; i < N ; i ++ ){
arr[i] = minHeap.extractMin();
System.out.print(arr[i] + " ");
}
System.out.println();
// 确保arr数组是从小到大排列的
for( int i = 1 ; i < N ; i ++ )
assert arr[i-1] <= arr[i];
}
}
- Lazy Prim
import java.util.Vector;
/**
* 使用Prim算法求图的最小生成树
* @author Liucheng
* @since 2019-10-16
*/
public class LazyPrimMST {
private WeightedGraph G; // 图的引用接口
private MinHeap> pq; // 最小堆, 算法辅助数据结构
private boolean[] marked; // 标记数组, 在算法运行过程中标记节点i是否被访问
private Vector> mst; // 最小生成树所包含的所有边
private Number mstWeight; // 最小生成树的权值
/**
* 构造函数
*/
public LazyPrimMST(WeightedGraph graph) {
// 初始化
this.G = graph;
this.pq = new MinHeap>(graph.E());
this.marked = new boolean[graph.V()];
this.mst = new Vector<>();
this.mstWeight = 0;
// 最小生成树算法
this.minTree();
}
/**
* 使用Prim算法求图的最小生成树
*/
private void minTree() {
// 记录还需要找多少个边,总得需要找到v-1个边即可停止查找
int edgeNeedFind = this.G.V() - 1;
// 从0节点进行切分
visit(0);
while (edgeNeedFind > 0) {
// 从最小堆中取出堆顶的边
Edge weightEdge = this.pq.extractMin();
/*这条边中其中一个节点是之前被访问过的(否则也不会添加到堆中),
而另一条边可能未被访问,也可能被访问,如果被访问,则堆中的此边就为非横切边*/
// 检查取出的边两点是否都被访问
if (this.marked[weightEdge.w()] == this.marked[weightEdge.v()]) {
continue;
}
this.mst.add(weightEdge);
if (!marked[weightEdge.w()]) {
this.visit(weightEdge.w());
} else {
this.visit(weightEdge.v());
}
edgeNeedFind --;
}
// 求总的权
// 计算最小生成树的权值
this.mst.forEach(
edge -> this.mstWeight = this.mstWeight.doubleValue() + edge.wt().doubleValue());
}
/**
* 访问节点v
*/
private void visit(int v) {
assert !marked[v];
marked[v] = true;
// 将和节点v相连的所有未访问的边放入最小堆中
for (Edge edge : G.adj(v)) {
/* 访问v点连接的另一条边的点!比如边(v <-> m), edge.other(v) 返回的就是m
这里添加此点时没有被访问,后续如果此方法被访问时传入的参数就为edge.other(v);那么
之前被添加的一条边的两个端点都被访问了,就不能作为 横切边!!!
*/
if (!marked[edge.other(v)]) {
pq.insert(edge);
}
}
}
/**
* 返回最小生成数的所有边
*/
public Vector> mstEdges() {
return mst;
}
/**
* 返回最小生成树的权值
*/
public Number result() {
return mstWeight;
}
public static void main(String[] args) {
String filename = Thread.currentThread().getContextClassLoader().getResource("testG1.txt").getPath();
int V = 8;
SparseWeightedGraph g = new SparseWeightedGraph(V, false);
ReadWeightedGraph readGraph = new ReadWeightedGraph(g, filename);
// Test Lazy Prim MST
System.out.println("Test Lazy Prim MST:");
LazyPrimMST lazyPrimMST = new LazyPrimMST(g);
Vector> mst = lazyPrimMST.mstEdges();
for( int i = 0 ; i < mst.size() ; i ++ ) {
System.out.println(mst.elementAt(i));
}
System.out.println("The MST weight is: " + lazyPrimMST.result());
System.out.println();
}
}
- maven工程resources下的测试文件testG1.txt
8 16
4 5 .35
4 7 .37
5 7 .28
0 7 .16
1 5 .32
0 4 .38
2 3 .17
1 7 .19
0 2 .26
1 2 .36
1 3 .29
2 7 .34
6 2 .40
3 6 .52
6 0 .58
6 4 .93
- 执行结果
Test Lazy Prim MST:
0-7: 0.16
7-1: 0.19
0-2: 0.26
2-3: 0.17
7-5: 0.28
5-4: 0.35
2-6: 0.4
The MST weight is: 1.81
Lazy Prim的时间复杂度为O(ElogE);E为图的边数
三、Prim 算法的优化
1). 优化思想
上述Lazy Prim的实现中,每访问一个节点,就将与该节点上的横切边添加到堆中,对于一条横切边,可能横切边上另一个切分上的点已经有了横切边,这时另一个切分上的点就存在两个横切边;而寻找最小生成树的那条横切边是目前所有横切边中最小的那一条横切边,此时就可以在这里做文章,只保留另一个切分上的点中最小的横切边,从而减少最小堆的比较次数!也就是说,每次访问一个节点后,对于另一个切分上,存在横切边的点有且只有一条最短的横切边!这样不仅大大减少了参与计算的横切边,同时将上述Lazy Prim堆中的存在的非横切边也给干掉了!
最短路径prim优化.png
2). 代码实现
- 辅助数据结构,索引堆
import java.lang.*;
/**
* 最小索引堆
* @author Liucheng
* @since 2019-10-19
*/
public class IndexMinHeap- {
protected Item[] data; // 最小索引堆中的数据
protected int[] indexes; // 最小索引堆中的索引, indexes[x] = i 表示索引i在x的位置
protected int[] reverse; // 最小索引堆中的反向索引, reverse[i] = x 表示索引i在x的位置
protected int count;
protected int capacity;
// 构造函数, 构造一个空堆, 可容纳capacity个元素
public IndexMinHeap(int capacity){
this.data = (Item[])new Comparable[capacity+1];
this.indexes = new int[capacity+1];
this.reverse = new int[capacity+1];
for(int i = 0;i <= capacity;i ++ ) {
reverse[i] = 0;
}
count = 0;
this.capacity = capacity;
}
// 返回索引堆中的元素个数
public int size(){
return count;
}
// 返回一个布尔值, 表示索引堆中是否为空
public boolean isEmpty(){
return count == 0;
}
/* 向最小索引堆中插入一个新的元素, 新元素的索引为i, 元素为item
传入的i对用户而言,是从0索引的*/
public void insert(int i, Item item){
assert count + 1 <= capacity;
assert i + 1 >= 1 && i + 1 <= capacity;
// 再插入一个新元素前,还需要保证索引i所在的位置是没有元素的。
assert !this.contain(i);
i += 1;
data[i] = item;
indexes[count+1] = i;
reverse[i] = count + 1;
count ++;
shiftUp(count);
}
// 从最小索引堆中取出堆顶元素, 即索引堆中所存储的最小数据
public Item extractMin(){
assert count > 0;
Item ret = data[indexes[1]];
swapIndexes( 1 , count );
reverse[indexes[count]] = 0;
count --;
shiftDown(1);
return ret;
}
// 从最小索引堆中取出堆顶元素的索引
public int extractMinIndex(){
assert count > 0;
int ret = indexes[1] - 1;
swapIndexes( 1 , count );
reverse[indexes[count]] = 0;
count --;
shiftDown(1);
return ret;
}
// 获取最小索引堆中的堆顶元素
public Item getMin(){
assert count > 0;
return data[indexes[1]];
}
// 获取最小索引堆中的堆顶元素的索引
public int getMinIndex(){
assert count > 0;
return indexes[1]-1;
}
// 看索引i所在的位置是否存在元素
boolean contain( int i ){
assert i + 1 >= 1 && i + 1 <= capacity;
return reverse[i+1] != 0;
}
// 获取最小索引堆中索引为i的元素
public Item getItem( int i ){
assert contain(i);
return data[i+1];
}
// 将最小索引堆中索引为i的元素修改为newItem
public void change( int i , Item newItem ){
assert contain(i);
i += 1;
data[i] = newItem;
// 有了 reverse 之后,
// 我们可以非常简单的通过reverse直接定位索引i在indexes中的位置
shiftUp( reverse[i] );
shiftDown( reverse[i] );
}
// 交换索引堆中的索引i和j
// 由于有了反向索引reverse数组,
// indexes数组发生改变以后, 相应的就需要维护reverse数组
private void swapIndexes(int i, int j){
int t = indexes[i];
indexes[i] = indexes[j];
indexes[j] = t;
reverse[indexes[i]] = i;
reverse[indexes[j]] = j;
}
//********************
//* 最小索引堆核心辅助函数
//********************
// 索引堆中, 数据之间的比较根据data的大小进行比较, 但实际操作的是索引
private void shiftUp(int k){
while( k > 1 && data[indexes[k/2]].compareTo(data[indexes[k]]) > 0 ){
swapIndexes(k, k/2);
k /= 2;
}
}
//********************
//* 最小索引堆核心辅助函数
//********************
// 索引堆中, 数据之间的比较根据data的大小进行比较, 但实际操作的是索引
private void shiftDown(int k){
while( 2*k <= count ){
int j = 2*k;
if( j+1 <= count && data[indexes[j+1]].compareTo(data[indexes[j]]) < 0 ) {
j ++;
}
if( data[indexes[k]].compareTo(data[indexes[j]]) <= 0 ) {
break;
}
swapIndexes(k, j);
k = j;
}
}
}
- 优化后的Prim思想实现
import java.util.Iterator;
import java.util.Vector;
/**
* 使用优化的Prim算法求图的最小生成树
* @author Liucheng
* @since 2019-10-19
*/
public class PrimMST {
private WeightedGraph G; // 图的引用
private IndexMinHeap ipq; // 最小索引堆, 算法辅助数据结构
private Edge[] edgeTo; // 访问的点所对应的边, 算法辅助数据结构
private boolean[] marked; // 标记数组, 在算法运行过程中标记节点i是否被访问
private Vector> mst; // 最小生成树所包含的所有边
private Number mstWeight; // 最小生成树的权值
// 构造函数,初始化
public PrimMST(WeightedGraph graph) {
this.G = graph;
this.ipq = new IndexMinHeap<>(graph.V());
this.edgeTo = new Edge[graph.V()];
this.marked = new boolean[graph.V()];
this.mst = new Vector<>(graph.V() - 1);
this.mstWeight = 0;
this.minTree();
}
// 使用Prim算法求图的最小生成树
private void minTree() {
this.visit(0);
while (!this.ipq.isEmpty()) {
// 使用最小索引堆找出已经访问的边中权值最小的边
// 最小索引堆中存储的是点的索引, 通过点的索引找到相对应的边
int minIndex = this.ipq.extractMinIndex();
this.mst.add(this.edgeTo[minIndex]);
this.visit(minIndex);
}
// 计算最小生成树的权值
this.mst.forEach(
edge -> this.mstWeight = this.mstWeight.doubleValue() + edge.wt().doubleValue());
}
// 访问节点v
private void visit(int v) {
assert !this.marked[v];
this.marked[v] = true;
// 将和节点v相连接的未访问的另一端点, 和与之相连接的边, 放入最小堆中
Iterator adj = this.G.adj(v).iterator();
while (adj.hasNext()) {
Edge next = (Edge)adj.next();
int other = next.other(v);
// 如果边的另一端点未被访问
if (!this.marked[other]) {
// 如果从没有考虑过这个端点, 直接将这个端点和与之相连接的边加入索引堆
if (this.edgeTo[other] == null) {
this.edgeTo[other] = next;
this.ipq.insert(other, next.wt());
// 如果曾经考虑这个端点, 但现在的边比之前考虑的边更短, 则进行替换
}else if (next.compareTo(this.edgeTo[other]) < 0) {
this.edgeTo[other] = next;
this.ipq.change(other, next.wt());
}
}
}
}
// 返回最小生成树的所有边
public Vector> mstEdges(){
return mst;
}
// 返回最小生成树的权值
public Number result(){
return mstWeight;
}
// 测试 Prim
public static void main(String[] args) {
String filename = Thread.currentThread().getContextClassLoader().getResource("testG1.txt").getPath();
int V = 8;
SparseWeightedGraph g = new SparseWeightedGraph(V, false);
ReadWeightedGraph readGraph = new ReadWeightedGraph(g, filename);
// Test Prim MST
System.out.println("Test Prim MST:");
PrimMST primMST = new PrimMST(g);
Vector> mst = primMST.mstEdges();
for( int i = 0 ; i < mst.size() ; i ++ )
System.out.println(mst.elementAt(i));
System.out.println("The MST weight is: " + primMST.result());
System.out.println();
}
}
- 测试执行结果
Test Prim MST:
0-7: 0.16
7-1: 0.19
0-2: 0.26
2-3: 0.17
7-5: 0.28
5-4: 0.35
2-6: 0.4
The MST weight is: 1.81
3). 复杂度分析
优化后的复杂度为O(ElogV); E为边数,V为点数
四、Krusk算法【思想简单,容易实现】
1). 算法思想
寻找最小生成树,把所有的边进行顺序排序,从小到大开始找v-1条边(v是点数),如果再找的过程找的当前边导致产生了环,就将当前的边摒弃并继续开始找!
最小生成树之Kruskal.png
2). 代码实现
辅助数据结构,最小堆 (见Lazy Prim实现)
辅助数据结构,并查集
/**
* @author Liucheng
* @since 2019-10-19
*/
public class UnionFind {
private int[] rank;
private int[] parent; // parent[i]表示第i个元素所指向的父节点
private int count; // 数据个数
// 构造函数
public UnionFind(int count){
this.rank = new int[count];
this.parent = new int[count];
this.count = count;
// 初始化, 每一个parent[i]指向自己, 表示每一个元素自己自成一个集合
for(int i = 0 ; i < count; i ++ ){
this.parent[i] = i;
this.rank[i] = 1;
}
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
private int find(int p){
assert(p >= 0 && p < count);
// path compression 1
while(p != parent[p]){
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if(pRoot == qRoot)
return;
// 根据两个元素所在树的元素个数不同判断合并方向
// 将元素个数少的集合合并到元素个数多的集合上
if(rank[pRoot] < rank[qRoot]){
parent[pRoot] = qRoot;
}
else if(rank[qRoot] < rank[pRoot]){
parent[qRoot] = pRoot;
}
else{ // rank[pRoot] == rank[qRoot]
parent[pRoot] = qRoot;
rank[qRoot] += 1; // 此时, 我维护rank的值
}
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
public boolean isConnected(int p , int q){
return this.find(p) == this.find(q);
}
}
- Krusk算法实现
import java.util.Vector;
/**
* @author Liucheng
* @since 2019-10-19
*/
public class KruskalMST {
private WeightedGraph graph; // 图的引用
private MinHeap> minHeap; // 最小堆
private UnionFind unionFind; // 并查集,检查已经访问的节点的联通情况
private Vector> mst; // 最小生成树所包含的所有边
private Number mstWeight; // 最小生成树的权值
// 构造函数
public KruskalMST(WeightedGraph graph) {
this.graph = graph;
this.minHeap = new MinHeap<>(graph.E());
this.unionFind = new UnionFind(graph.V());
this.mst = new Vector<>(graph.V() - 1);
this.mstWeight = 0;
this.minTree();
}
// 使用Kruskal算法计算graph的最小生成树
public void minTree() {
// 将图中的所有边存放到一个最小堆中
for (int i = 0; i < this.graph.V(); i++) {
for (Edge weightEdge : this.graph.adj(i)) {
// 无向图中,添加一条边时实际上是创建了两个对象,表示不同点的邻边连接方向
// 但是在此算法逻辑中只求两点的距离,所以得防止在一个堆中多添加一个边的“反向边”
if (weightEdge.v() <= weightEdge.w()) {
this.minHeap.insert(weightEdge);
}
}
}
// 从最小堆中取出最小的距离,并检验是否成环
while (!this.minHeap.isEmpty() && this.mst.size() < this.graph.V() - 1) {
// 从最小堆中依次从小到大取出所有的边
Edge e = this.minHeap.extractMin();
// 如果该边的两个端点是联通的, 说明加入这条边将产生环, 扔掉这条边
if (this.unionFind.isConnected(e.v(), e.w())) {
continue;
}
// 否则, 将这条边添加进最小生成树, 同时标记边的两个端点联通
this.mst.add(e);
this.unionFind.unionElements(e.v(), e.w());
}
// 计算最小生成树的权值
this.mst.forEach(
edge -> this.mstWeight = this.mstWeight.doubleValue() + edge.wt().doubleValue()
);
}
// 返回最小生成树的所有边
public Vector> mstEdges() {return mst;}
// 返回最小生成树的权值
public Number result() {return mstWeight;}
// 测试 Kruskal
public static void main(String[] args) {
String filename = Thread.currentThread().getContextClassLoader().getResource("testG1.txt").getPath();
int V = 8;
SparseWeightedGraph g = new SparseWeightedGraph(V, false);
ReadWeightedGraph readGraph = new ReadWeightedGraph(g, filename);
// Test Kruskal
System.out.println("Test Kruskal:");
KruskalMST kruskalMST = new KruskalMST(g);
Vector> mst = kruskalMST.mstEdges();
for( int i = 0 ; i < mst.size() ; i ++ )
System.out.println(mst.elementAt(i));
System.out.println("The MST weight is: " + kruskalMST.result());
System.out.println();
}
}
- 测试结果
Test Kruskal:
0-7: 0.16
2-3: 0.17
1-7: 0.19
0-2: 0.26
5-7: 0.28
4-5: 0.35
2-6: 0.4
The MST weight is: 1.81
3). 复杂度
O(ElogE + ElogV) -> O(ElogE),对于小的图可以使用Krusk算法实现
五、最小生成树的思考
1). 总结
- Lazy Prim O(ElogE)
- Prim O(ElogV)
- Kruskal O(ElogE)
假如在横切边中,同时存在多个最短的横切边,以上实现的算法可以满足,也因此这种情况下,图存在多个最小生成树
2). 最短路径的其他思想 Vyssotsky's Algorithm
将边之间地添加到生成树中,一旦形成环,就删除环中权值最大的边