最小生成树(Minimum Span Tree)
- 针对带权无向图
- 针对连通图
切分
把途中的结点分为两部分,成为一个切分(Cut)
如果一个边的两个端点,属于切分不同的两边,这个边称为横切边(Crossing Edge)
切分定理
给定任意切分,横切边中权值最小的边必然属于最小生成树。
Lazy Prim
最小堆
/*
* Created by wxn
* 2018/12/16 3:16
*/
/**
* 最小堆
*/
public class MinHeap<T extends Comparable<T>> {
private T data[];
private Integer count;
private Integer capacity;
public MinHeap(Integer capacity) {
this.capacity = capacity;
data = (T[]) new Comparable[capacity + 1];
count = 0;
}
public int size() {
return count;
}
public boolean isEmpty() {
return count == 0;
}
public void insert(T item) {
assert count < capacity;
data[count + 1] = item;
count++;
shiftUp(count);
}
public T extractMin() {
assert count > 0;
T ret = data[1];
swap(1, count);
count--;
shiftDown(1);
return ret;
}
private void shiftDown(int k) {
while (k * 2 <= count) {
int j = k * 2;
if (j + 1 <= count && data[j + 1].compareTo(data[j]) < 0) {
j++;
}
if (data[k].compareTo(data[j]) > 0) {
swap(j, k);
k = j;
} else {
break;
}
}
}
private void shiftUp(int k) {
while (k > 1 && data[k / 2].compareTo(data[k]) > 0) {
swap(k, k / 2);
k /= 2;
}
}
// 交换堆中索引为i和j的两个元素
private void swap(int i, int j) {
T t = data[i];
data[i] = data[j];
data[j] = t;
}
public static void main(String args[]) {
Integer[] arr = new Integer[20];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) (Math.random() * 100);
}
MinHeap<Integer> minHeap = new MinHeap<>(arr.length);
for (int i = 0; i < arr.length; i++) {
minHeap.insert(arr[i]);
}
for (int i = 0; i < arr.length; i++) {
System.out.print(minHeap.extractMin() + " ");
}
}
}
LazyPrimMst
/*
* Created by wxn
* 2018/12/16 3:43
*/
import java.util.Vector;
/**
* LazyPrim最小生成树
*/
public class LazyPrimMst<Weight extends Number & Comparable<Weight>> {
private WeightGraph<Weight> graph; // 图的引用
private MinHeap<Edge<Weight>> minHeap; // 最小堆, 算法辅助数据结构
private boolean[] mark; // 标记数组, 在算法运行过程中标记节点i是否被访问
private Vector<Edge<Weight>>mst; // 最小生成树所包含的所有边
private Number mstWeight; // 最小生成树的权值
public LazyPrimMst(WeightGraph<Weight> graph){
this.graph = graph;
minHeap = new MinHeap<>(graph.E());
mark = new boolean[graph.V()];
mst = new Vector<>();
visit(0);
while (!minHeap.isEmpty()){
// 使用最小堆找出已经访问的边中权值最小的边
Edge<Weight> e = minHeap.extractMin();
// 如果这条边的两端都已经访问过了, 则扔掉这条边
if (mark[e.v()]==mark[e.w()]){
continue;
}
// 否则, 这条边则应该存在在最小生成树中
mst.add(e);
// 访问和这条边连接的还没有被访问过的节点
if (!mark[e.v()]){
visit(e.v());
}else {
visit(e.w());
}
}
// 计算最小生成树的权值
mstWeight = mst.get(0).wt();
for (int i = 1 ; i<mst.size(); i++){
mstWeight = mstWeight.doubleValue() + mst.get(i).wt().doubleValue();
}
}
private void visit(int v) {
assert !mark[v];
mark[v] = true;
//将所有和节点v相连的未访问的边放入最小堆中
for (Edge<Weight> e : graph.adj(v)) {
if (!mark[e.other(v)]){
minHeap.insert(e);
}
}
}
public static void main(String args[]) {
WeightGraph<Double>graph = new SparseWeightedGraph<>(8,false);
String filename = "TestG1.txt";
ReadWeightedGraph readWeightedGraph = new ReadWeightedGraph(graph,filename);
graph.show();
LazyPrimMst<Double> lazyPrimMst = new LazyPrimMst<>(graph);
for (Edge<Double> edge : lazyPrimMst.mst) {
System.out.println(edge);
}
System.out.println("total weight: " + lazyPrimMst.mstWeight);
}
}
运行结果
与预期相符
优化的Prim算法
/*
* Created by wxn
* 2018/12/16 21:22
*/
/**
* 最小索引堆
*/
public class IndexMinHeap<T extends Comparable<T>> {
private T[] data; //最小索引堆中的数据
private int[] indexes; //最小索引堆中的索引,indexes[x]=i表示索引i在x的位置
private int[] reverse; //最小索引堆中的反向索引,reverse[i]=x表示索引i在x的位置
private int count;
private int capacity;
public IndexMinHeap(int capacity) {
this.capacity = capacity;
data = (T[]) new Comparable[capacity + 1];
indexes = new int[capacity + 1];
reverse = new int[capacity + 1];
for (int i = 0; i <= capacity; i++) {
reverse[i] = 0;
}
count = 0;
}
// 返回索引堆中的元素个数
public int size() {
return count;
}
// 返回一个布尔值, 表示索引堆中是否为空
public boolean isEmpty(){
return count == 0;
}
//向最小索引堆中插入一个新元素,索引为i 元素为item
//传入的i对用户而言是从0索引的
public void insert(int i, T item) {
assert count + 1 <= capacity;
assert i + 1 >= 1 && i + 1 <= capacity;
assert !contain(i);
i++;
data[i] = item;
indexes[count+1] = i;
reverse[i] = count+1;
count++;
shiftUp(count);
}
// 从最小索引堆中取出堆顶元素, 即索引堆中所存储的最小数据
public T extractMin(){
assert count > 0;
T 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 T getMin(){
assert count > 0;
return data[indexes[1]];
}
// 获取最小索引堆中的堆顶元素的索引
public int getMinIndex(){
assert count > 0;
return indexes[1]-1;
}
// 索引堆中, 数据之间的比较根据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;
}
}
private void shiftUp(int k) {
while (k>1 && data[indexes[k/2]].compareTo(data[indexes[k]])>0){
swapIndexes(k,k/2);
k/=2;
}
}
// 交换索引堆中的索引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;
}
// 看索引i所在的位置是否存在元素
private boolean contain(int i) {
assert i + 1 >= 1 && i + 1 <= capacity;
return reverse[i+1] != 0;
}
// 获取最小索引堆中索引为i的元素
public T getItem( int i ){
assert contain(i);
return data[i+1];
}
// 将最小索引堆中索引为i的元素修改为newItem
public void change( int i , T newItem ){
assert contain(i);
i += 1;
data[i] = newItem;
// 有了 reverse 之后,
// 我们可以非常简单的通过reverse直接定位索引i在indexes中的位置
shiftUp( reverse[i] );
shiftDown( reverse[i] );
}
// 测试 IndexMinHeap
public static void main(String[] args) {
int N = 1000000;
IndexMinHeap<Integer> indexMinHeap = new IndexMinHeap<>(N);
for( int i = 0 ; i < N ; i ++ )
indexMinHeap.insert( i , (int)(Math.random()*N) );
}
}
/*
* Created by wxn
* 2018/12/16 21:39
*/
import java.util.Vector;
// 使用优化的Prim算法求图的最小生成树
public class PrimMst<Weight extends Number & Comparable<Weight>> {
private WeightGraph graph; //图的引用
private IndexMinHeap<Weight> indexMinHeap; //最小索引堆
private Edge<Weight>[] edgeTo; //访问的点所对应的边
private boolean[] marked; //标记数组,在算法运行过程中标记节点i是否被访问
private Vector<Edge<Weight>> mst; //最小生成树所包含的所右边
private Number mstWeight; //最小生成树的权值
public PrimMst(WeightGraph graph) {
this.graph = graph;
assert graph.E()>=1;
indexMinHeap = new IndexMinHeap<>(graph.V());
marked = new boolean[graph.V()];
edgeTo = new Edge[graph.V()];
for (int i = 0; i < graph.V(); i++) {
marked[i] = false;
edgeTo[i] = null;
}
mst = new Vector<>();
//Prim
visit(0);
while (!indexMinHeap.isEmpty()){
// 使用最小索引堆找出已经访问的边中权值最小的边
// 最小索引堆中存储的是点的索引, 通过点的索引找到相对应的边
int v = indexMinHeap.extractMinIndex();
assert edgeTo[v]!=null;
mst.add(edgeTo[v]);
visit(v);
}
// 计算最小生成树的权值
mstWeight = mst.elementAt(0).wt();
for( int i = 1 ; i < mst.size() ; i ++ )
mstWeight = mstWeight.doubleValue() + mst.elementAt(i).wt().doubleValue();
}
// 访问节点v
private void visit(int v) {
assert !marked[v];
marked[v] = true;
// 将和节点v相连接的未访问的另一端点, 和与之相连接的边, 放入最小堆中
for (Object item : graph.adj(v)) {
Edge<Weight> e = (Edge<Weight>) item;
int w = e.other(v);
//如果边的另一端点未被访问
if (!marked[w]){
// 如果从没有考虑过这个端点, 直接将这个端点和与之相连接的边加入索引堆
if (edgeTo[w]==null){
edgeTo[w] = e;
indexMinHeap.insert(w,e.wt());
}
// 如果曾经考虑这个端点, 但现在的边比之前考虑的边更短, 则进行替换
else if( e.wt().compareTo(edgeTo[w].wt()) < 0 ){
edgeTo[w] = e;
indexMinHeap.change(w, e.wt());
}
}
}
}
// 返回最小生成树的所有边
Vector<Edge<Weight>> mstEdges(){
return mst;
}
// 返回最小生成树的权值
Number result(){
return mstWeight;
}
// 测试 Prim
public static void main(String[] args) {
String filename = "testG1.txt";
int V = 8;
SparseWeightedGraph<Double> g = new SparseWeightedGraph<Double>(V, false);
ReadWeightedGraph readGraph = new ReadWeightedGraph(g, filename);
// Test Prim MST
System.out.println("Test Prim MST:");
PrimMst<Double> primMST = new PrimMst<>(g);
Vector<Edge<Double>> 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();
}
}
运行结果
结果与之前相同
Kruskal Mst
原理
- 将图的所有边按权值从小到大排序,依次遍历这些边。如果将遍历到的边加入到MST中没有形成环,那么这条边就是最小生成树的一条边。
- 如何判断是否形成环?
利用并查集(UnionFind):如果这条边的两个顶点在并查集中相连的话,那么就会形成环。
代码
/*
* Created by wxn
* 2018/12/16 22:32
*/
/**
* 并查集
*/
public class UnionFind {
// rank[i]表示以i为根的集合所表示的树的层数
// 在后续的代码中, 我们并不会维护rank的语意, 也就是rank的值在路径压缩的过程中, 有可能不在是树的层数值
// 这也是我们的rank不叫height或者depth的原因, 他只是作为比较的一个标准
private int[] rank;
private int[] parent; //parent[i]表示第i个元素所指向的父节点
private int count;
public UnionFind(int count) {
this.count = count;
rank = new int[count];
parent = new int[count];
for (int i = 0; i < count; i++) {
parent[i] = i;
rank[i] = 0;
}
}
// 查找过程, 查找元素p所对应的集合编号
// O(h)复杂度, h为树的高度
int find(int p) {
assert p >= 0 && p < count;
//path compression
while (p != parent[p]) {
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
// 查看元素p和元素q是否所属一个集合
// O(h)复杂度, h为树的高度
boolean isConnected(int p, int q) {
return find(p) == find(q);
}
// 合并元素p和元素q所属的集合
// O(h)复杂度, h为树的高度
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[pRoot]>rank[qRoot]){
parent[qRoot] = pRoot;
}
else { //rank[pRoot]=rank[qRoot]
parent[pRoot] = qRoot;
rank[qRoot]++;
}
}
}
/*
* Created by wxn
* 2018/12/16 22:43
*/
import java.util.Vector;
/**
* Kruskal算法求最小生成树
*/
public class KruskalMst<Weight extends Number & Comparable<Weight>> {
private Vector<Edge<Weight>> mst; //最小生成树所包含的所右边
private Number mstWeight; //最小生成树的权值
public KruskalMst(WeightGraph graph) {
mst = new Vector<>();
//将图中所有边存放到一个最小堆中
MinHeap<Edge<Weight>> minHeap = new MinHeap<>(graph.E());
for (int i = 0; i < graph.V(); i++) {
for (Object item : graph.adj(i)) {
Edge<Weight> e = (Edge<Weight>) item;
if (e.v() < e.w()) {
minHeap.insert(e);
}
}
}
//创建一个并查集,查看已经访问的节点的联通情况
UnionFind uf = new UnionFind(graph.V());
while (!minHeap.isEmpty() && mst.size() < graph.V() - 1) {
//从最小堆中一次从小到大取出所有边
Edge<Weight> e = minHeap.extractMin();
// 如果该边的两个端点是联通的, 说明加入这条边将产生环, 扔掉这条边
if (uf.isConnected(e.v(), e.w())) {
continue;
}
// 否则, 将这条边添加进最小生成树, 同时标记边的两个端点联通
mst.add(e);
uf.unionElements(e.v(), e.w());
}
// 计算最小生成树的权值
mstWeight = mst.elementAt(0).wt();
for (int i = 1; i < mst.size(); i++)
mstWeight = mstWeight.doubleValue() + mst.elementAt(i).wt().doubleValue();
}
// 返回最小生成树的所有边
Vector<Edge<Weight>> mstEdges(){
return mst;
}
// 返回最小生成树的权值
Number result(){
return mstWeight;
}
// 测试 Kruskal
public static void main(String[] args) {
String filename = "testG1.txt";
int V = 8;
SparseWeightedGraph<Double> g = new SparseWeightedGraph<Double>(V, false);
ReadWeightedGraph readGraph = new ReadWeightedGraph(g, filename);
// Test Kruskal
System.out.println("Test Kruskal:");
KruskalMst<Double> kruskalMST = new KruskalMst<>(g);
Vector<Edge<Double>> 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();
}
}
运行结果
拓展:Vyssotsky’ s Algorithm
- 将边逐渐地添加到生成树中,一旦形成环,删除环中权值最大的边。
时间复杂度
- LazyPrimMst: O(ElogE)
- PrimMst: O(ElogV)
- Kruskal: O(ElogE)
我的github仓库:
https://github.com/649733108/Mook-Play-with-Algorithms/tree/master/WeightedGraph/src
欢迎骚扰!