演算法導論 第23章 最小生成樹 思考題
23-1 次最優的最小生成樹
(c)根據普利姆演算法計算出最小生成樹,並得到樹的parent陣列(裡面記錄了各頂點的父頂點)。
運用動態規劃方法即可,狀態轉移方程如下:
設頂點個數為V,那麼時間複雜度為O(V2) 。
(d)
1、根據普利姆演算法計算得出最小生成樹,並得到max[u,v];
2、對於沒有加入到MST中的每條邊,設為(x,y),計算出,稱為邊權差EdgeWeightGap[x,y]。
3、從EdgeWeightGap中選出權值差最小的,然後在MST中刪除max[x,y]那條邊,並加入(x,y)這條邊,即得到次最優的最小生成樹。
23-2 稀疏圖的最小生成樹
題目:
分析:
我們將這個演算法分成四個過程來分析,分析完了,也就明白了題目是什麼意思,以及orig什麼的到底是啥。我們所用的圖是本章的那個圖,如下,按字母順序用數字1.2.3...編號:
第一個過程:演算法1~3行
對每個頂點初始化並查集和訪問標誌域;
第二個過程:演算法4~9行
1、檢查每一個頂點的訪問標誌域mark,若已被設定則結束,不掃描,繼續下一個頂點,否則轉2;
2、掃描該頂點的鄰接連結串列(按照鄰接點從到小的順序掃描),找到與其鄰接的最小權值邊(設為(u,v))後,轉3;
3、將u,v兩端點合併到同一個集合;將邊(u,v)直接加到MST中,和演算法些許不同,見稍後解釋;將兩個端點的訪問標誌都置上,意味著頂點v的鄰接連結串列之後不會被掃描了,結束後轉1.。
關於第3步和演算法不同的原因:此時沒有必要對這樣的邊也設定orig屬性,因為它們並不和收縮後的圖的邊對應。該過程結束後,T中呈現如下景象:
其中,紅色的頂點是其所在樹的樹根。可以看見,現在的最小生成樹的雛形已經出現,它是一個森林。之後的過程才會對這個森林進行收縮,因此這些邊不需要設定orig屬性。
第三個過程:演算法第10行
這個過程就是找出T森林中的各棵樹的樹根,根據之前的並查集來查詢。由第三個過程可以得到樹根分別為2,9,7,它們將會是收縮圖G'中的頂點,在此,我們將這三個頂點重新編號為1,2,3,便於後面的prim演算法的執行。
圖我就不畫了。
第四個過程:演算法11~22行
這個過程的目的是獲得收縮圖G'的邊,也就是上述幾個根的聯絡,它們通過各自樹中節點的最小權值邊聯絡。
1、掃描原圖中的每一條邊(x,y),沒有邊了,轉5;否則,找到它們所屬的樹的根,分別為u,v,轉2;
2、若u和v相同,意味著它們同屬於一棵樹,轉1;否則,轉3;
3、若邊(u,v)不存在於E',說明這兩棵樹還沒有建立聯絡,那麼自然加入該邊,設定orig記錄邊(u,v)和它實際所引用的原圖的邊(x,y),權值也記錄下來;若存在,則轉4;
4、找到這個orig,將w(x,y)和orig中記錄的權值比較,若較小,則更改orig的引用邊以及權值,因為這兩棵樹之間出現了更小的聯絡代價;否則,不變。轉1;
5、根據orig建立G'的鄰接表,結束。
經過第三和第四個過程,演算法進展如下:
1、T中各棵樹已經收縮,每棵樹收縮成一個頂點,由根代表,整個森林成為一棵樹,即G';
2、G'的各頂點由原圖中除加入T中的各邊之外的最小權值邊聯絡著,orig記錄了這些聯絡。
到了這個過程結束,可以得到下面的圖,左圖是G',邊上數字表示該邊的權;右圖是加入orig的記錄的圖,圈中數字是T中各棵樹的樹根,紅色頂點是它們在圖G'中的新編號,邊上的(x,y)表示這些樹是通過原圖的某邊聯絡的,數字就是該邊的權。
預處理過程到這裡就結束了,得到G'和orig,之後採用prim演算法求G'的MST,然後將它的邊全部加入T中,加入之前要根據orig換成原圖的邊加入,最後得到原圖的最小生成樹T。
該演算法的C++實現程式碼如下,註釋詳細,小題解答見後面:
#include<iostream>
#include<algorithm>
#include<fstream>
#include<vector>
#include<queue>
#include<map>
#include"FibonacciHeap.h"
#define NOPARENT 0
#define MAX 0x7fffffff
using namespace std;
enum color{ WHITE, GRAY, BLACK };
struct edgeNode
{//邊節點
size_t adjvertex;//該邊的關聯的頂點
size_t weight;//邊權重
edgeNode *nextEdge;//下一條邊
edgeNode(size_t adj, size_t w) :adjvertex(adj), weight(w), nextEdge(nullptr){}
};
struct findRoot:public binary_function<vector<size_t>,size_t,size_t>
{//函式物件類,用於查詢並查集
size_t operator()(const vector<size_t> &UFS, size_t v)const
{
while (v != UFS[v]) v = UFS[v];
return v;
}
};
struct edge
{//邊,和edgeNode有別
size_t u, v;
size_t weight;
edge(size_t u_, size_t v_, size_t w) :u(u_), v(v_), weight(w){}
};
struct edgeRef
{//在preMST和MST23_2過程用到
size_t u, v;//邊
size_t x, y;//及其引用邊
size_t weight;
size_t u_map, v_map;//u,v的新編號
edgeRef(size_t u_, size_t v_, size_t x_, size_t y_,
size_t w,size_t u_m = 0,size_t v_m = 0) :u(u_), v(v_), x(x_), y(y_),
weight(w),u_map(u_m),v_map(v_m){}
};
class AGraph
{//無向圖
private:
vector<edgeNode*> graph;
size_t nodenum;
void transformGraph(vector<edge>&);
void preMST(AGraph*, AGraph*, vector<edgeRef>&);
public:
AGraph(size_t n = 0){editGraph(n); }
void editGraph(size_t n)
{
nodenum = n;
graph.resize(n + 1);
}
size_t size()const { return nodenum; }
void initGraph();//初始化無向圖
edgeNode* search(size_t, size_t);//查詢邊
void add1Edge(size_t, size_t, size_t);//有向圖中新增邊
void add2Edges(size_t, size_t, size_t);//無向圖中新增邊
size_t prim(AGraph*,size_t);
void mst23_2(AGraph *mst);
void print();
void destroy();
~AGraph(){ destroy(); }
};
void AGraph::initGraph()
{
size_t start, end;
size_t w;
ifstream infile("F:\\mst.txt");
while (infile >> start >> end >> w)
add1Edge(start, end, w);
}
void AGraph::transformGraph(vector<edge> &E)
{
for (size_t i = 1; i != graph.size(); ++i)
{//改造edgeNode,變成edge
edgeNode *curr = graph[i];
while (curr != nullptr)
{
if (i < curr->adjvertex)
{//頂點u,v之間的邊只儲存一條,(u,v),且u < v。
edge e(i, curr->adjvertex, curr->weight);
E.push_back(e);
}
curr = curr->nextEdge;
}
}
}
edgeNode* AGraph::search(size_t start, size_t end)
{
edgeNode *curr = graph[start];
while (curr != nullptr && curr->adjvertex != end)
curr = curr->nextEdge;
return curr;
}
void AGraph::add1Edge(size_t start, size_t end, size_t weight)
{
edgeNode *curr = search(start, end);
if (curr == nullptr)
{
edgeNode *p = new edgeNode(end, weight);
p->nextEdge = graph[start];
graph[start] = p;
}
}
inline void AGraph::add2Edges(size_t start, size_t end, size_t weight)
{
add1Edge(start, end, weight);
add1Edge(end, start, weight);
}
size_t AGraph::prim(AGraph *mst, size_t u)
{//普利姆演算法求最小生成樹,採用斐波那契堆。返回最小權值和;mst儲存最小生成樹,時間O(E+VlgV)
vector<size_t> parent(nodenum + 1);
//儲存每個頂點在斐波那契堆中的對應節點的地址,這樣便於修改距離等
vector<fibonacci_heap_node<size_t, size_t>*> V(nodenum + 1);
fibonacci_heap<size_t, size_t> Q;//斐波那契堆,鍵為距離,值為頂點標號
for (size_t i = 1; i <= nodenum; ++i)
{
parent[i] = i;
if (i == u) V[i] = Q.insert(0, i);//向堆中插入元素,並且將節點控制代碼存入陣列
else V[i] = Q.insert(MAX, i);
}
size_t sum = 0;
while (!Q.empty())
{
pair<size_t, size_t> min = Q.extractMin();
V[min.second] = nullptr;//置空,標誌著該節點已刪除
sum += min.first;
for (edgeNode *curr = graph[min.second]; curr; curr = curr->nextEdge)
{//以其為中介,更新各點到MST的距離
if (V[curr->adjvertex] != nullptr && curr->weight < V[curr->adjvertex]->key)
{
Q.decreaseKey(V[curr->adjvertex], curr->weight);
parent[curr->adjvertex] = min.second;
}
}//將該邊加入MST
if (min.second != u) mst->add2Edges(parent[min.second], min.second, min.first);
}
return sum;
}
void AGraph::preMST(AGraph *T, AGraph *G, vector<edgeRef> &orig)
{//稀疏圖求MST預處理,T儲存mst,G儲存收縮後的圖,orig儲存收縮後的圖的邊,以及它所引用的原圖的邊
//和該邊權值,注意該過程結束後mst並未完全求出。
vector<color> mark(nodenum + 1);//訪問標誌
vector<size_t> ufs(nodenum + 1);//並查集
for (size_t i = 1; i <= nodenum; ++i)
{
mark[i] = WHITE;
ufs[i] = i;
}
//-------------------------------------------------------
for (size_t i = 1; i != graph.size(); ++i)
{//一次掃描每個頂點
if (mark[i] == WHITE)
{//若未訪問,
edgeNode *curr = graph[i];
size_t u = 0, w = MAX;
while (curr != nullptr)
{//則一次訪問其鄰接表,
if (curr->weight < w)
{//找到最短的邊
u = curr->adjvertex;
w = curr->weight;
}
curr = curr->nextEdge;
}
T->add2Edges(i, u, w);//將其加入到T中成為mst的一條邊
ufs[i] = u;//並設定並查集
mark[i] = mark[u] = BLACK;//且標為訪問
}
}//該過程結束後,T是森林,儲存了一些mst的邊,森林中樹的根則在ufs中可以查到
//-------------------------------------------------------------------------
map<size_t, size_t> V_of_G;//記錄圖G的頂點,即T中森林中各樹的樹根,鍵為樹根編號,值為其在收縮後的圖的編號
size_t num_of_V = 0;
for (size_t i = 1; i != ufs.size(); ++i)
{//掃描ufs
size_t p = findRoot()(ufs, i);//找尋各頂點的根,
map<size_t, size_t>::iterator it = V_of_G.find(p);
if (it == V_of_G.end())//若沒有記錄則加入,並一次編號為1,2,3...便於之後的處理,故用map儲存
V_of_G.insert(pair<size_t, size_t>(p, ++num_of_V));
}
//------------------------------------------------------------------------------
vector<edge> E;
transformGraph(E);//該函式在原圖的鄰接表中抽取所有的邊
for (size_t i = 0; i != E.size(); ++i)
{//依次訪問這些邊
size_t u_root = findRoot()(ufs, E[i].u), v_root = findRoot()(ufs, E[i].v),j;//找到改變兩頂點的根
if (u_root == v_root) continue;//若相等,說明該邊已存在於mst中,則不處理,繼續掃描下一條邊
for (j = 0; j != orig.size(); ++j)//否則查詢是否以存入orig
if ((orig[j].u == u_root && orig[j].v == v_root)
|| (orig[j].u == v_root && orig[j].v == u_root)) break;
if (j == orig.size())
{//若沒有,則新增,其中(u_root,v_root),是G中的邊,其引用的是E[i]這條邊
edgeRef er(u_root, v_root, E[i].u, E[i].v, E[i].weight);
orig.push_back(er);
}
else if (E[i].weight < orig[j].weight)
{//若存在,且新邊比之前的引用邊的權值更小,則更改引用邊資訊
orig[j].x = E[i].u;
orig[j].y = E[i].v;
orig[j].weight = E[i].weight;
}
}//該過程結束後,orig記錄了T中森林之間的聯絡,以及該聯絡引用的權值最小的邊
//------------------------------------------------------------------------
G->editGraph(num_of_V);//根據頂點數目重新編輯收縮圖G的大小
for (size_t i = 0; i != orig.size(); ++i)
{//根據orig,構造出圖G的鄰接表,此時用樹根的相應編號構造圖G,便於後續處理
map<size_t, size_t>::iterator it1 = V_of_G.find(orig[i].u), it2 = V_of_G.find(orig[i].v);
orig[i].u_map = it1->second; orig[i].v_map = it2->second;//記下orig中u和v的編號
G->add2Edges(it1->second, it2->second, orig[i].weight);
}
}
void AGraph::mst23_2(AGraph *T)
{//稀疏圖求mst
AGraph G;
vector<edgeRef> orig;
preMST(T, &G, orig);//呼叫預處理過程以求得MST雛形,儲存於T中;收縮後的圖G,以及G中的引用邊orig
AGraph mst_G(G.size());
G.prim(&mst_G,1);//對圖G用普利姆演算法求出MST
for (size_t i = 1; i != mst_G.graph.size(); ++i)
{//依次掃描G的MST的每個頂點
edgeNode *curr = mst_G.graph[i];
while (curr != nullptr)
{//若該頂點有鄰接表
size_t j;
//由於圖G的頂點是經過編號的,為1,2,3...,因而要找出它在原圖中的頂點標號
for (j = 0; j != orig.size(); ++j)
if (i == orig[j].u_map && curr->adjvertex == orig[j].v_map)
//找到後,在T中加入該邊的的引用邊————T中森林是用該引用邊聯絡起來的
//根據引用邊的求取過程,可以知道每條引用邊是聯絡這兩棵樹的最小權值邊
T->add2Edges(orig[j].x, orig[j].y, orig[j].weight);
curr = curr->nextEdge;
}
}
}//結束後即構造出稀疏圖的MST
inline void AGraph::print()
{
for (size_t i = 1; i != graph.size(); ++i)
{
edgeNode *curr = graph[i];
cout << i;
if (curr == nullptr) cout << " --> null";
else
while (curr != nullptr)
{
cout << " --<" << curr->weight << ">--> " << curr->adjvertex;
curr = curr->nextEdge;
}
cout << endl;
}
}
void AGraph::destroy()
{
for (size_t i = 1; i != graph.size(); ++i)
{
edgeNode *curr = graph[i], *pre;
while (curr != nullptr)
{
pre = curr;
curr = curr->nextEdge;
delete pre;
}
graph[i] = curr;
}
}
const size_t nodenum = 9;
size_t main()
{
AGraph graph(nodenum), mst(nodenum);
graph.initGraph();
graph.print();
cout << endl;
graph.mst23_2(&mst);
mst.print();
getchar();
return 0;
}
(a).T是由各個鄰接連結串列中的最小權值邊構成的森林,A中是該森林連線圖的最小生成樹,把這裡的邊加入T,可以滿足權值和最小,且不會形成環。
(b).由第二個過程可知,只要掃描到一個鄰接連結串列,找到其中的最小權值邊後,即將兩個端點均標為訪問,可以得知,森林中的每棵樹至少有兩個頂點,即最多有|V|/2棵樹。
(c).採用按秩合併和路徑壓縮實現並查集。
(d).由c可知每個階段執行時間為O(E),則k個階段時間自然為O(kE)。
(e).採用斐波那契堆實現prim演算法,執行時間為O(E'+V'lgV'),執行k次MST-REDUCE時間為O(kE'),故總時間為O(kE') + O(E'+V'lgV') = O(kE'+V'lgV') = O(kE+V'lgV'),由於每執行一次MST-REDUCE,頂點數目至少減半,故k次後,V' <= ((1/2)^k)V,因此當k = lglgV時,時間為O(ElglgV)。
(f).O(ElglgV) < O(E+VlgV),得:E < VlgV / lglgV。