算法导论23章思考题(转载)
23-1次优最小生成树
a.
最小生成树唯一性证明:
已知当前构造的边集A是最小生成树的子集。令无向图G的一个切割是,显然该切割是尊重A的。已知跨越该切割的轻量级边对于A是安全的,又因为该无向图G的每条边的权值都不相同,所以对于当前A而言,安全边有且只有一条,即对于每个状态下的A,构造最小生成树的方式是唯一的。所以最小生成树是唯一的。
次优最小生成树不唯一性证明:
如上图:{(C, D), (A, D), (A, B)} 和 {(C, D), (A, C), (B, D)} 是两个次优最小生成树,权值和都是8。
b.
①如果最小生成树T删去一条边,就必然要添加另一条边,否则不能形成一个连通块。
②如果最小生成树T和次小生成树有两条边不同,即T' = T - {(u1, v1)} + {(x1, y1)} - {(u2, v2)} + {(x2, y2)},则可以构造出一棵和最小生成树只有一条边不同的生成树T'' = T - {(u1, v1)} + {(x1, y1)},使得w(T) < w(T'') < w(T')。这和T'是次小生成树矛盾,所以次小生成树和最小生成树只有一条边不同。
③由①②可知,图G包含边(u, v)属于T和边(x, y)不属于T,使得T - {(u, v)} + {(x, y)}是G的一棵次小生成树。
c. 假设当前已经构造的最小生成树的子集为A,维护max_edge数组,max_edge[i][j]表示max(i, j)。按照Prim算法,新添加进了一条边(u, v),就可以利用维护的信息计算出A中任意一个点k到新添加的点v的max(k, v)。G[i][j]表示边(i, j)的长度。
计算公式为:max(k, v) = max(max(k, u), G[u][v])
d. 算法:假设当前求出的最小生成树为T,枚举所有不属于T的边(u, v),向T中添加(u, v)。
因为会形成环,所以要删掉一条边。因为我们希望得到的生成树权值最小,所以要 删掉环中权值最大的边,也就是max_edge(u, v),然后就会得到新的生成树T'。在得到的所有T'中,权值和最小的就是次小生成树。
代码如下(POJ1679):
#include <algorithm> #include <iostream> #include <cstdlib> #include <cstring> #include <cstdio> using namespace std; const int MAX_N = 500; const int INF = 0x3f3f3f3f; // used[i][j] = 1表示最小生成树中包含(i, j)这条边。 int used[MAX_N][MAX_N]; // 题目中输入的图 int G[MAX_N][MAX_N]; // max_edge[i][j]表示在最小生成树中i到j的唯一简单路径中权值最大的边长。 int max_edge[MAX_N][MAX_N]; // 标记i是否使用过 int vis[MAX_N]; // mincost[i]表示i到已构造的最小生成树子集的最短距离。 int mincost[MAX_N]; // (pre[i], i)为i到已构造的最小生成树子集的最短边。 int pre[MAX_N]; // n为顶点数,m为边数。 int n, m; // ans1为最小生成树的权值和,ans2为次小生成树的权值和。 int ans1, ans2; // 初始化 void init() { memset(vis, 0, sizeof(vis)); memset(used, 0, sizeof(used)); memset(mincost, INF, sizeof(mincost)); memset(max_edge, 0, sizeof(max_edge)); for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) G[i][j] = (i == j) ? 0 : INF; } // 求最小生成树的权值和 int MST() { mincost[1] = 0; pre[1] = 1; int ret = 0; for (int cnt = 1; cnt <= n; cnt++) { int minval = INF, k; for (int i = 1; i <= n; i++) { if (!vis[i] && minval > mincost[i]) minval = mincost[k = i]; } // 提前结束循环,说明不存在最小生成树。 if (minval == INF) return -1; // 标记(pre[k], k)这条边已经使用过。 used[k][pre[k]] = used[pre[k]][k] = 1; vis[k] = 1; ret += minval; for (int i = 1; i <= n; i++) { // 如果i在已构造的子集中,就利用维护的max_edge信息求出max_edge[i][k]。 if (vis[i]) max_edge[i][k] = max_edge[k][i] = max(max_edge[i][pre[k]], G[pre[k]][k]); else if (G[k][i] != INF && mincost[i] > G[k][i]) { mincost[i] = G[k][i]; pre[i] = k; } } } return ret; } // 求次小生成树的权值和 int second_MST() { int ret = INF; for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) { // 如果i, j之间有未使用过的边,就添加(i, j),但这个时候会形成环, // 所以要删除环中最长的一条边,即max_edge[i][j]。 if (i != j && G[i][j] != INF && !used[i][j]) ret = min(ret, ans1 + G[i][j] - max_edge[i][j]); } return ret; } int main() { //freopen("t1.txt", "r", stdin); int T; scanf("%d", &T); while (T--) { scanf("%d%d", &n, &m); init(); for (int i = 0; i < m; i++) { int u, v, w; scanf("%d%d%d", &u, &v, &w); G[u][v] = G[v][u] = w; } ans1 = MST(); ans2 = second_MST(); // 如果次小生成树等于最小生成树,说明最小生成树不唯一。 if (ans1 == ans2) printf("Not Unique!\n"); else printf("%d\n", ans1); } return 0; }
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; }
23-3 瓶颈生成树
无向图的最小生成树一定是瓶颈生成树,但瓶颈生成树不一定是最小生成树。(最小瓶颈生成树==最小生成树)
命题:无向图的最小生成树一定是瓶颈生成树。
命题:瓶颈生成树不一定是最小生成树。
解:(a) 使用替换法,假设瓶颈树T'的最大边长为W,而某棵MST的最大边>W,则将MST从最大边切断,变成两个部分,选择T'中连接两部分的边,则得到更小的树。
(b) 运行DFS(或BFS),计算过程中忽略所有大于b的边。
(c) 二分+收缩法:将边按大小分成两半,在较小的那一半上进行BFS,如果得到的是几个连通分量,那么将每个连通分量收缩成一个点;如果得到的一棵树,那么再将边减半。
#23-4 其他MST算法
解:参考这里http://mypathtothe4.blogspot.com/2013/04/alternative-minimum-spanning-trees-234.html
(a) 正确,考虑算法运行的任意阶段,对于任意割,如果有多个边跨越割,那么先删除的永远是最大的那条边,即轻量边会一直留下;另,每次删除的边一定是某个环里面的最大边。Algorithm Design by Kleinberg 定理4.21 有详细证明。
(b) 错误。比如一个三角形,第一次选择了最长的那条边。
(c) 正确。