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。