两种 Prim 算法的区别
- 基于最小索引堆优化的 Prim 算法在堆中最多存储 v-1 条边,即每个节点最多存一条邻边到堆中;而 LazyPrim 算法会将一个节点的多条邻边存进堆中;
- 由于堆中存储边的数量从 e 变为 v,从而关于堆的操作的时间复杂度从 O(logE) 降为 O(logV);
- v 是比 e 小的;
存储在文件中的图
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
优化的 Prim 算法 - O(ElogV)
void visit(int v)
- 访问节点 v,并把 v 所有相邻节点(除了已经纳入 MST 的)的最小权边维护进数组
edgeTo
中;
- 随着访问的节点越来越多,
edgeTo
中有值的节点越来越多,在 edgeTo
中已经有值的节点的值在更新,更新成权更小的边;
核心算法逻辑描述
- 在探索图的过程中,把探索到的边的权值更新在以节点为横坐标的一维数组中,
- 从 0 开始探索;
- 每次探索完了,取出权最小的边纳入 MST;
- 然后以新纳入的边的另一端开始新的探索;
要点
- 最小索引堆
ipq
在带权无向图中的几何意义是:除了已经纳入 MST 的边,已经 visit 到的边;
- 每次从最小索引堆
ipq
删除的边就是要纳入 MST 的;
- 最小索引堆
ipq
中存的是遍历到某个程度时,节点当前探索到的权最小的邻边的权值,存储在最小索引堆的 data
数组中,data
数组的每一格表示图的一个节点;
-
data
数组中的值根据大小,以最小堆的结构维护在数组 indexes
中;
-
indexes
数组中元素的个数,对应的是最小堆中元素的个数,对应到图中是涉及到(不是被 visit 到)的点的数目;
package _08._05;
import java.util.Vector;
// 使用优化的Prim算法求图的最小生成树
public class PrimMST {
private WeightedGraph G; // 图的引用
private IndexMinHeap ipq; // 最小索引堆, 算法辅助数据结构
private Edge[] edgeTo; // 访问的点所对应的边, 算法辅助数据结构
private boolean[] marked; // 标记数组, 在算法运行过程中标记节点i是否被访问
private Vector> mst; // 最小生成树所包含的所有边
private Number mstWeight; // 最小生成树的权值
// 构造函数, 使用Prim算法求图的最小生成树
public PrimMST(WeightedGraph graph){
G = graph;
assert( graph.E() >= 1 );
ipq = new IndexMinHeap(graph.V());
// 算法初始化
marked = new boolean[G.V()];
edgeTo = new Edge[G.V()];
for( int i = 0 ; i < G.V() ; i ++ ){
marked[i] = false;
edgeTo[i] = null;
}
mst = new Vector>();
// 核心算法 - 优化过的 Prim 算法
visit(0);
while( !ipq.isEmpty() ){
/**
* v 是已经遍历过的边中权最小的边那头的端点(不属于 MST 的那头),这个端点 v 是将要纳入 MST 的点, 这个端点 v 当前正在考虑的边
* edgeTo[v] 是要纳入 MST 的边;
*/
int v = ipq.extractMinIndex();
// edgeTo[v] 是端点 v 当前正在考虑的边;
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 是已经纳入 MST 中的节点;
* 对 v 的每条边,取到其另一个端点 w;
* 对还没有纳入 MST 的 w:
* 如果 w 的任何一条临边是否可以纳入 MST 都没有被考虑过:
* 先把从 v 指向 w 的边 e 作为 w 当前考虑的边维护进数组 edgeTo 中;
* 再在最小索引堆中维护 w 节点当前考虑的边的权;
* 如果已经有临接于 w 的边被考虑过,并且从 v 指向 w 的边 e 的权比当前 w 正在考虑的边的权小:
* 更新 w 正在考虑的边为 e;
* 在最小索引堆中更新 w 当前考虑的临边的权;
*
* 对于第一个切分中的属于 MST 的唯一的点,比如点 v, 经过 visit(int v),v 的所有临边那头的点都把和 v 之间的边 e 的权视为最小的,随着
* 图中的点不断的被 visit(int v), 已经维护了最小权边的点会更新其维护的最小权边;
* @param v
*/
void visit(int v){
assert !marked[v];
marked[v] = true;
// 将和节点v相连接的未访问的另一端点, 和与之相连接的边, 放入最小堆中
for( Object item : G.adj(v) ){
Edge e = (Edge)item;
int w = e.other(v);
// 如果边的另一端点未被访问
if( !marked[w] ){
// 如果从没有考虑过这个端点, 直接将这个端点和与之相连接的边加入索引堆
if( edgeTo[w] == null ){
edgeTo[w] = e;
ipq.insert(w, e.wt());
}
// 如果曾经考虑这个端点, 但现在的边比之前考虑的边更短, 则进行替换
else if( e.wt().compareTo(edgeTo[w].wt()) < 0 ){
edgeTo[w] = e;
ipq.change(w, e.wt());
}
}
}
}
// 返回最小生成树的所有边
Vector> mstEdges(){
return mst;
}
// 返回最小生成树的权值
Number result(){
return mstWeight;
}
// 测试 Prim
public static void main(String[] args) {
String filename = "testG1.txt";
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();
}
}
测试
package _08._05;
public class Main {
// 测试我们实现的两种Prim算法的性能差距
// 可以看出这一节使用索引堆实现的Prim算法优于上一小节的Lazy Prim算法
public static void main(String[] args) {
String filename1 = "src/_08/_05/testG1.txt";
int V1 = 8;
// 文件读取
SparseWeightedGraph g1 = new SparseWeightedGraph(V1, false);
ReadWeightedGraph readGraph1 = new ReadWeightedGraph(g1, filename1);
System.out.println( filename1 + " load successfully.");
long startTime, endTime;
// Test Prim MST
System.out.println("Test Prim MST:");
startTime = System.currentTimeMillis();
PrimMST primMST1 = new PrimMST(g1);
endTime = System.currentTimeMillis();
System.out.println("Test for G1: " + (endTime-startTime) + "ms.");
System.out.println("Test for G1's mstWeight: " + primMST1.result());
}
}
输出:
src/_08/_05/testG1.txt load successfully.
Test Prim MST:
Test for G1: 3ms.
Test for G1's mstWeight: 1.81
辅助类
最小索引堆 - IndexMinHeap
package _08._05;
import java.lang.reflect.Array;
import java.util.*;
import java.lang.*;
// 最小索引堆
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){
data = (Item[])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;
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 !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;
}
}
// 测试 IndexMinHeap
public static void main(String[] args) {
int N = 1000000;
IndexMinHeap indexMinHeap = new IndexMinHeap(N);
for( int i = 0 ; i < N ; i ++ )
indexMinHeap.insert( i , (int)(Math.random()*N) );
}
}