一、最佳化內容
- 在prim演算法中,使用while迴圈對每一個節點進行遍歷,其中對邊進行出堆操作進行了E遍,堆鄰邊進行遍歷為E遍,總體來說prim的時間複雜度為:O(ElogE)。
- 在prim中會對每一條邊加入堆中,但取出時,取出的邊有可能不再是橫切邊;那麼我們現在要維護一個資料結構用來儲存和鄰節點連線最短的橫切邊,在不斷的擴大紅色節點時只需維護更新每個節點相連的最短的橫切邊就可以了;而這個資料結構能夠取出最小值,又能讓其元素更新——最小索引堆(Min Index Heap)。
二、prim的最佳化過程
如下圖:
最小索引堆開節點大小的空間用來存放v-1的邊,將0空閒
從0開始:
最小索引堆為空,將橫切邊依次放入最小索引堆中:
取出最小索引堆堆頂元素0.16,並將其退出最小索引堆
然後將最小生成樹中的邊 0.16相連的未被訪問節點加入紅色,並依次遍歷節點7相連的邊:
在最小索引堆中和節點1相連的值為空,則將新產生的橫切中最小的橫切7-1邊0.19放入最小索引堆中
遍歷7-2邊0.34,此時最小索引堆中和節點2相連邊的權值為0.26 這時需要比較0.26 < 0.34,所以將0.34丟棄。
- 此時再看圖中邊7-4邊0.37,跟最小索引堆中和節點4相連的邊0.38比較,0.37 < 0.38, 所以將最小索引堆中和節點4相連的邊更新為0.37,與此同時也相當於把0.38丟棄了(基於切分定理),如圖:
- 再看圖中7—5的邊為0.28,而在最小索引堆中節點5為空,則將0.28加入堆中:
然後再清楚最小索引堆中堆頂元素0.19,加入最小生成樹中,這時就需要將邊0.19相連且未被訪問過的節點加入紅色:
將節點1加入紅色陣營後,然後依次對與節點1相連且未被訪問過的邊進行遍歷:
對圖中1-2邊0.36訪問,在最小索引堆中與節點2相連的邊0.26;0.26 < 0.36 , 將0.36丟棄
如下圖:
- 然後對1-5邊0.32訪問,此時在最小索引堆中和節點5相連的邊0.28; 0.28 < 0.32,將0.32丟棄:
如下圖,丟棄0.32:
- 下面對1-3邊0.29訪問,在最小索引堆中與節點3相連的節點為空,直接加入堆中:
下面再一次從最小索引堆中將最小元素取出:0.26放入最小生成樹中
對和邊0.26相連且未被訪問的節點加入紅色陣營
同理對未被訪問過的邊進行遍歷,比較,放入,取出,最後得:
同理,按照步驟繼續,執行,最後就可得到最小生成樹:
這就是最佳化後的prim。
三、程式碼實現
這裡需要一個輔助資料結構——最小索引堆
#include <iostream>
#include <algorithm>
#include <cassert>
using namespace std;
//最小索引堆
template <typename Item>
class IndexMinHeap{
private:
Item data; //資料陣列
int count; //資料對應索引
int capacity; //堆的容量
int *indexes; //最小索引堆中的索引,indexes[x] = i 表示索引在x的位置
int *reverse; // 最小索引堆中的反向索引, reverse[i] = x 表示索引i在x的位置
// 索引堆中, 資料之間的比較根據data的大小進行比較, 但實際操作的是索引
void shiftUP(int k){
while(data[indexes[k]] < data[indexes[k/2]]){
swap(indexes[k], indexes[k/2]);
reverse[indexes[k/2]] = k/2;
reverse[indexes[k]] = k;
k /= 2;
}
}
// 索引堆中, 資料之間的比較根據data的大小進行比較, 但實際操作的是索引
void shiftDown(int k){
//2*k <= count 至少存在左孩子
while(2*k <= count){
int j = 2*k;
//右孩子存在時
if(data[indexes[j]] > data[indexes[j+1]]){
j += 1;
}
//只有左孩子時
if(data[indexes[j]] >= data[indexes[k]]){
break;
}
swap(indexes[k], indexes[j]);
reverse[indexes[k]] = k;
reverse[indexes[j]] = j;
k = j;
}
}
public:
//建構函式
IndexMinHeap(int capacity){
data = new Item[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;
}
//解構函式
~IndexMinHeap(){
delete[] data;
delete[] indexes;
delete[] reverse;
}
//返回堆中元素個數
int size(){
return count;
}
//返回一個布林值,表示索引堆是否為空
bool isEmpty(){
return count == 0;
}
//向最小索引堆中插入新元素,新元素索引為i,元素為item
//傳入的對於使用者來說是從0索引的
void insert(int index, Item item){
index += 1;
data[index] = item;
indexes[count+1] = index;
//reverse[index] = count+1 表示用來記錄真正元素的索引對應在索引堆中的位置
reverse[index] = count+1;
count ++;
shiftUP(count);
}
// 從最小索引堆中取出堆頂元素, 即索引堆中所儲存的最小資料
Item extractMin(){
assert(count > 0);
Item ret = data[indexes[1]];
swap(indexes[1], indexes[count]);
count--;
reverse[indexes[count]] = 0;
reverse[indexes[1]] = 1;
shiftDown(count);
return ret;
}
//獲取最小索引堆中堆頂元素
Item getMin(){
assert(count > 0);
return data[indexes[1]];
}
//獲取最小索引堆中堆頂元素的索引
int getIndexes(){
assert(count > 0);
return indexes[1]-1;
}
//看索引所在位置是否存在元素
bool contain(int index){
return reverse[index+1] != 0;
}
// 獲取最小索引堆中索引為i的元素
Item getItem(int index){
assert(contain(index));
return data[index+1];
}
//將最小索引堆中索引為i元素修改成newItem
void change(int index, Item newItem){
assert( contain(index) );
index += 1;
data[index] = newItem;
shiftUP(reverse[index]);
shiftDown(reverse[index]);
}
};
下面是prim的最佳化:
#include <iostream>
#include <vector>
#include <cassert>
#include "Edge.h"
#include "IndexMinHeap.h"
using namespace std;
// 使用最佳化的Prim演算法求圖的最小生成樹
template<typename Graph, typename Weight>
class PrimMST{
private:
Graph &G; // 圖的引用
IndexMinHeap<Weight> ipq; // 最小索引堆, 演算法輔助資料結構
vector<Edge<Weight>*> edgeTo; // 訪問的點所對應的邊, 演算法輔助資料結構
bool* marked; // 標記陣列, 在演算法執行過程中標記節點i是否被訪問
vector<Edge<Weight>> mst; // 最小生成樹所包含的所有邊
Weight mstWeight; // 最小生成樹的權值
// 訪問節點v
void visit(int v){
assert( !marked[v] );
marked[v] = true;
// 將和節點v相連線的未訪問的另一端點, 和與之相連線的邊, 放入最小堆中
typename Graph::adjIterator adj(G,v);
for( Edge<Weight>* e = adj.begin() ; !adj.end() ; e = adj.next() ){
int w = e->other(v);
// 如果邊的另一端點未被訪問
if( !marked[w] ){
// 如果從沒有考慮過這個端點, 直接將這個端點和與之相連線的邊加入索引堆
if( !edgeTo[w] ){
edgeTo[w] = e;
ipq.insert(w, e->wt());
}
// 如果曾經考慮這個端點, 但現在的邊比之前考慮的邊更短, 則進行替換
else if( e->wt() < edgeTo[w]->wt() ){
edgeTo[w] = e;
ipq.change(w, e->wt());
}
}
}
}
public:
// 建構函式, 使用Prim演算法求圖的最小生成樹
PrimMST(Graph &graph):G(graph), ipq(IndexMinHeap<double>(graph.V())){
assert( graph.E() >= 1 );
// 演算法初始化
marked = new bool[G.V()];
for( int i = 0 ; i < G.V() ; i ++ ){
marked[i] = false;
edgeTo.push_back(NULL);
}
mst.clear();
// Prim
visit(0);
while( !ipq.isEmpty() ){
// 使用最小索引堆找出已經訪問的邊中權值最小的邊
// 最小索引堆中儲存的是點的索引, 透過點的索引找到相對應的邊
int v = ipq.extractMinIndex();
assert( edgeTo[v] );
mst.push_back( *edgeTo[v] );
visit( v );
}
mstWeight = mst[0].wt();
for( int i = 1 ; i < mst.size() ; i ++ ){
mstWeight += mst[i].wt();
}
}
//解構函式
~PrimMST(){
delete[] marked;
}
vector<Edge<Weight>> mstEdges(){
return mst;
};
Weight result(){
return mstWeight;
};
};
本作品採用《CC 協議》,轉載必須註明作者和本文連結