1. 程式人生 > >演算法導論 第23章 最小生成樹 思考題

演算法導論 第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。