算法|算法导论(第三版)-复习- 第六部分图论 22-26[转]

22 习题22.1-5 有向图G(V, E)的平方图。链表表示时,对每结点u的Adj[u]中所有v加入队列,后边出队边将Adj[v]加入Adj[u]中。矩阵表示时,若w[i, j]、w[j, k]同时为1则将w[i, k]置1.
习题22.1-6 O(V)的时间寻找通用汇点。汇点的特征是邻接矩阵的第j列除[j, j]外所有元素为1. 可将每行调整[j ,j]后作为一个整数,所有整数与运算,为1的位是汇点。
习题22.1-7 有向无环图的关联矩阵B,BB’每个元素C[i, j]=∑B[i, k]*B’[k, j]=∑B[i, k]*B[j, k],即同时进i, j两结点与同时出i, j的结点总数-一进一出i, j两结点的结点总数。
习题22.2-7 类似BFS,d mod2为0则标为B(娃娃脸),d mod2为1则标为H(高跟鞋)。但若有边连接相同类的结点,则无法划分。

wrestler(G){ for each u in G{ (u,v)=Adj[u]; if(v.mark==u.mark){throw error; } if(v.d==NIL) {v.d=u.d+1; v.mark=v.d mod 2; } } }

习题22.2-8 任意点之间的最短路径。重复的Dijktra算法或Floyd-Warshall算法
习题22.2-9 无向图扩展为有向图。问题变成要遍历所有边一次。访问结点u时,将u的子结点v的其他边都可视为子集v,问题等价于u到v,访问v的集合,v到u。u标为visiting入列,然后访问v,v标为visiting入列,然后访问v的后继结点,访问过的边标为visited,返回到visiting的点时,如果该点所有连接的边都标为visited只剩一条返回上级的边,则返回上级结点并将点标为visited,v出列,访问u的其他子结点,最终u出列。全部结点出列后达到遍历所有边一次。
expand all (u, v) to (v, u); visit_all_paths(G){ Q.push(u0); while(Q not full){ u=Q.pop(); visit(u); } } visit(u){ for each (u, v) in Adj[u]{ if((u, v) not visited){ if(v not visiting){ mark v as visiting; v.p=u; Q.push(v); mark (u, v) as visited; visit(v); } if(v is visiting){mark (u, v) as visited; mark (v, u) as visited; } } } mark u as visited; Q.pop(); }

习题22.3-7 重写DFS算法,用栈代替递归。
DFS(G){ for each vertex u in G.V{ while(u not visited){ if(u not visiting) {mark u as visiting; insert all Adj[u] to stack[u]; } if(stack[u] not empty){ v=stack[u].pop(); if(v neither visited nor visiting) {v.p=u; u=v; continue; } } else {mark u as visited; if(u.p not null) u=u.p; } } } }

习题22.3-13 判断有向图是否单连通图。访问到已标记为visited的结点时,回溯到根结点,检查有无公共的根结点。
习题22.4-2 拓扑排序后,从起点开始计数,s有边连接的点,路径为1,然后向后推导,某点v路径数为v的所有入边(u,v)的前驱点u的路径相加。从s到t,得到路径数量。
习题22.4-5 可以用邻接矩阵w,入度为0的点,其列上元素无1,其行上元素无-1, 删掉一个点后,删去其行及其列。但若有环,无法拓扑排序。
习题22.5-6 首先,各强连通分量收缩为一个点,然后将这些点之间的边删除。然后对每个连通分量去掉出入度大于1的点的多余边。获得最少|V|的G’。
习题22.5-7 半连通问题。如果能够任意两点之间至少有一条路径到达,那么可以知道,一种无回溯的DFS遍历必然能到达所有点。反证法:若不能遍历,那么必有两点u到v之间无路径。
semi_connected_components(G){ while(count <= |V| ) { if(u not visiting) {mark u as visiting; push each Adj[u] in stack[u]; count++; } if(stack[u] not empty) {v=stack[u].pop(); u=v; continue; } else {mark u as visited; break; } } if(count!=|V|) throw error; }

思考题22-3 欧拉回路。1,v=Adj[u].pop()的方式寻找一个回路。每个路过的元素作为一个循环链表的结点。每个路过的结点in_degree与out_degree各-1,若有degree不等于0的,将其用递归方法展开,即从该结点开始寻找另一个子回路,并将子回路经过的路径再次加入循环链表的相应位置。如此直到所有边都被加入为止。
思考题22-4 从L标记为1点开始,按照BFS遍历,并更新可到达点的值。记录拥有最新min(R)=1元素个数i,那么下一次从L标记i+1个结点开始,BFS遍历。如此类推。
23 习题23.1-11 给定图G和一棵最小生成树T,假设减少了位于T之外的某条边的权重。因为T内的边,是连接所有结点的权重最小的,那么首先将T外的减少权重的边(u, v)加入T,然后在u, v中寻找所有的路径,去掉路径中权重最大的边。
习题23.2-3 使用斐波那契堆实现的Prim算法与使用二叉堆比较。因为斐波那契堆实现优先队列,使Prim算法的运行时间为O(E+VlgV),而二叉堆实现优先队列的Prim算法运行时间为O(VlgV+ElgV),前者更小。同时,稠密图使用斐波那契堆实现Prim算法更有优势。因为E越大,需要更新堆中元素次数越多。
习题23.2-4 边权重为1~|V|整数的图的Kruskal算法。边排序用计数排序,时间为O(E),有O(V)次make_set与O(E)次find_set和union,总时间为O(E+α(V)*(E+V))。如果边权重为1~W,则取决于W与|V|中最大值。
习题23.2-5 边权重为1~|V|整数的图的Prim算法。若最小优先队列用斐波那契堆,总运行时间为O(E+VlgV)。前一项代表执行更新堆中元素值的时间,后一项代表出堆时间。但若W
24 习题21.1-3 Bellman-Ford算法改进为m+1次松弛后终止。图中结点若在s->v的路径中则作标记。松弛过程中,若有标记的结点全部不更新v值,则停止。此时松弛次数为m+1趟。
习题21.1-5 松弛方法改为结点已有d值,对其所有入边选择w+ d< d 中最小的代替原有的d,按照Bellman-Ford算法运行V-1趟。
习题21.1-6 寻找权重为负值的环。用2维矩阵保存所有结点之间的最短路径,也包括自己到自己的路径。然后按照Bellman-Ford算法运行V-1趟松弛。L’(i, j)=min{L(i, k)+ w(k, j) },其中L’(i, j)是运行过程中,最新的i结点到j结点的最短路径。L(i, k)是上次运行得到的i结点到任一结点k的最短路径。L(i, i)是自己到自己的最短路径,L(i, i)为负值的结点,即是负值的环路上的结点。
习题24.2-4 计算有向无环图中的路径总数。拓扑排序后,从起点到终点,遍历每个结点同时计算路径数。P(v)=∑P(u),即结点的路径总数等于所有入边的前驱的路径数之和。
习题24.3-6 最可靠的通信链路。与原始的路径权重相加不同,通信链路上不失效的概率应该相乘。则可以将概率取10为底的对数,作为路径权重。然后用一般的单源最短路径算法就可以解决了。
习题24.3-8 权重函数w: {0, 1, 2,…, W},修改Dijkstra算法计算源点到所有结点最短路径,时间为O(W* V+ E)。因为路径权重都是0到W的自然数,所以优先队列可以用计数排序实现。则运行时间为O(W* V+ E)。其中,V*W是结点总数V乘每次从优先队列中取出时的代价W。E是松弛的总次数。
习题24.3-9 上题继续修改,满足运行时间O((V+E)lgW)。最小优先队列的实现方式改为一棵平衡二叉搜索树,每个结点代表0~W之间的一个自然数,插入时每放入一个元素,则结点计数加1,向上将父结点加1,直到根结点。查找时若左子结点的计数>0则向左下移动,否则返回本结点。如此则插入与查找的时间均为O(lgW)。Dijkstra算法需要将遍历V个结点,每次在二叉树中查找一次,并且对E条边每条都有一次插入二叉树操作。则总时间是O((V+ E)* lgW)。
【算法|算法导论(第三版)-复习- 第六部分图论 22-26[转]】习题24.4-4 单源最短路径问题表示为线性规划问题。对于任意边E(u, v),w(u, v)是从u到v的边的权重,则有方程xv- xu<= w(u, v)。现寻找s到t的最短路径,即xt-xs的最小值。Bellman-Ford算法可以解。
习题24.4-5 修改Bellman-Ford算法,使其能够在O(nm)时间内解决由n个未知变量和m个约束条件构成的差分约束系统。一个可行的方案是,将每个结点赋初值0,从而省略掉初始源点。因为初始源点只有出边,没有入边,所以迭代过程不会影响初始源点以及初始源点到任一点的路径权值。这样可以将边的数量降为m,结点数量降为n,总时间为O(n*m)。
习题24.4-9 Ax<= b为n个变量与m个约束条件的差分约束系统。证明Bellman-Ford算法可以得到max{x} - min{x}的最小值。首先,收敛过程保证了任意结点的权重是满足约束方程的一个解,那么在从初始源点到任意结点的一条路径上,两点i、j之间的权重差值,就是两个解的差值xi- xj 的最小值。遍历所有路径,可以找到在一条路径上的两个结点,他们之间的权重之差是max{x}- min{x}的最小值。
习题24.4-12 与24.4-11基本相似。Ax<= b的差分约束系统,b所有元素为实数,变量中的某给定子集为整数。先用Bellman-Ford算法得到一个实数的可行解。然后按照得到的路径反向遍历,从初始结点开始,每经过一条边,就将后一个结点的权重赋值为前一结点的权重加上路径权重。对于整数的结点,将结点权重取整,然后继续向后遍历。
G(u, v) Bellman-Ford(){ for i=1 to V-1 for each edge(u, v) in G relax(u, v, w); if(relaxed) u.p=v; //找到属于最短路径中的路径 } DFS_find_path(){ for each vertex v{ add v to v.p.child[]; //路径反向,构成源点到任意点的路径的森林 mark v as unvisited; } for each vertex v in source_s.child[] { mark v as visiting; insert all v.child[] in stack; } while(u=stack.pop()){ insert all u.child[] in stack; mark u as visiting; u.d= u.p.d+ w(u, v); if(u belong in integer_set) u=u]; //向下取整 if(u.child[] all mark as visted) mark u as visited; } }

习题24.5-8 G(V, E)为有向图,不包含权重为负值的环路。证明所有结点v,存在|V|-1个松弛步骤组成的松弛序列来生成v.d=δ(s, v)。用数学归纳法。首先证明若最短路径只有1条边情况,Initialize_Single_Source(G, s)就可以收敛。假设最短路径有n条边,经过n-1次松弛可以收敛。那么在最短路径为n+1条边的情况下,n-1次松弛得到v的前驱u的最短路径权重u.d,然后多1次松弛可以得到v.d。可见n+1条边的最短路径需松弛n次。全部结点最多需松弛|V|-1次。
思考题24-1 Yen对Bellman-Ford算法的改进。Gf中的任意边(i, j)都满足i< j的偏序关系,那么DFS深度遍历时,i.d>j.d,并且j的后继不可能是i,则Gf中无环,且拓扑排序时i一定在j的前面。Gb同理。
一遍松弛的过程,Gf与Gb都可以得到各自的最短生成路径。G上一条完整的最短路径,最多可以包含|V|-1条边,其中分属于Gf与Gb。可以知道,此完整路径只需要max{edges in Gf, edges in Gb }次松弛。则最多需要|V|/2次松弛可以得到完整的最短路径。
渐近时间要考虑排序的时间。首先是给所有结点一个随机数以确定偏序,时间O(|V|),然后给所有边标记Gf或Gb,时间O(|E|),最后是|V|/2趟松弛,总时间O(|V|+|E|+|V|*|E|/2),比原版Bellman-Ford算法节省了常数时间。
思考题24-2 嵌套盒子问题。可以先对两个盒子的元素排序,然后比较各相同位置的元素,对任意i<= A.size,A[i]< B[i],则A可以嵌套到B内。
最长嵌套盒子序列。所有盒子排序,然后给出一个拓扑排序,顺序基于两个盒子能否嵌套,有嵌套关系的盒子作为一条有向边。这样就转变成了寻找一条最长路径的问题。从起点到终点一趟松弛可以完成。
思考题24-3 套利交易问题。任意两种货币之间的汇率表示为双向的边,权重为汇率r[i, j]和r[j, i]= 1/r[i, j]。寻找一个环路,是否经过所有路径的权重乘积∏r >1。所有r转换为log对数坐标,则相乘转换为相加。然后Bellman-Ford算法寻找一个权重为负的环路即可。
思考题24-5 Karp的最小平均权重环路算法。u= min max(δn(s,v)- δk(s,v))/ (n-k)。对每个结点v, 令δ[k-1](s, u) 是其所有入边的前驱点u的拥有k-1条边的最短路径。则δ[k](s, v)是δ[k-1](s, u) + (u, v)的松弛结果。经过V-1轮松弛,可以得到k为从1到E的全部结点的全部δ[k],并计算出u,时间为O(VE)。
思考题24-6 双调最短路径。因为最短路径都是双调最短路径,所以沿着任意最短路径的边都是权重先增大再减小。松弛时把所有边按照权重增大的顺序松弛一次再按照权重减小的顺序松弛一次,就可以保证任意最短路径上的结点都经历了一次从起点到终点和一次从终点到起点的松弛过程。松弛次数为2就可以得到所有的最短路径。
25 习题25.1-6 O(n^3)时间内从已经计算出的最短路径权重矩阵L计算出前驱矩阵Π。任意的L[i, j]最短路径,若j结点前驱为k,则必然有L[i, j]= L[i, k]+ w[k, j]。如此可遍历L矩阵的第i行所有元素L[i, k],若L[i, j]= L[i, k]+ w[k, j],表明k是i-> j最短路径的j的前驱结点。
习题25.1-7 在Extend_Shortest_Paths方法中加入记录前驱结点的功能。
extend_shortest_paths(L, W){ for i= 1 to n for j=1 to n for k=1 to n if( L'[i, j]> L[i, k]+ W[k, j] ){ L'[i, j]= L[i, k]+ W[k, j]; P[i, j]=k; } return L', P } [plain] view plain copy slow_all_pairs_shortest_paths(){ L[1]=W; for m=2 to n-1 L[m], P[m]=extend_shortest_paths(L[m-1]) }

习题25.1-10 找到最短的权重为负值的环路的长度(边数)。最短路径矩阵L中若元素L[i, i]是负值,则表明i在一个负权重的环路中。同时对上面方法找到的前驱矩阵进行查找,得到环路的边数。
习题25.2-4 证明去掉上标的Floyd-Warshall算法是正确的,从而将空间需求降低到Θ(n^2)。对任意k,d[i, j]依赖于d[i, k]和d[k, j],d[i, k]与d[k, j]可能经历了k-1次松弛也可能经历了k次松弛。但不管如何,d[i, j]是中间结点取自{1, 2, …, k}的一条最短路径。此即循环不变量。则当k取n时,全部d[i, j]均得到了包含所有结点的最短路径。
习题25.2-8 给出一个O(VE)的时间复杂度的算法来计算有向图G(V, E)的传递闭包。对每个点u进行DFS或者BFS,若经过结点v则将T[u, v]标为1。可知最大运行时间为O(VE)。
习题25.3-5 Johnson算法,证明权重为0的环路上每条边的w’(u, v)=0. 首先,若环路只有一条边,首尾只有一个点,边的权重为0,w’为0. 若不止一个结点,因为环路权重为0,则w(u, v)= -w(v, u)。w’(u, v)= w(u, v)+ h(u)- h(v),则当w(u, v)< 0 时,δ(s, v)= δ(s, u)+ w(u, v),或当w(u, v)> 0 时,δ(s, v)= δ(s, u)- w(u, v)。换句话说,δ(s, v)一定包括w(u, v)或 δ(s, u)一定包括w(v, u)。否则会形成负值环路。则h(u)- h(v)= -w(u, v) 或h(v)- h(u)= w(u, v)。则可证得w’(u, v)= w’(v, u)=0。
思考题25-1 动态图的传递闭包。加入一条边(k, l),则扫描传递闭包矩阵中所有元素,只要满足T[u, k]=1同时T[l, v]=1,就将T[u, v] 赋为1。 运行时间O(V^2)。
insert_edge(E(k, l), T) { for u= 1 to n for v= 1 to n if T[u, k]==1 and T[l, v]==1 then T[u, v]=1 }

26 流网络,容量值,源结点,汇点,容量限制,流量守恒。反平行,超级源结点,超级汇点。
Ford-Fulkerson方法。残存网络,增广路径,最小切割定理。f是最大流,残存网络不包含增广路径,|f|等于最小切割容量三者等价。
基本的Ford-Fulkerson算法。Edmonds-Karp算法。为了算法的收敛性。残存网络中用广度优先寻找增广路径。证明运行时间为O(V*E^2):对特定一条边,其成为关键边的次数最多为V/2,残存网络最多有O(E)种可能的关键边,每条增广路径至少一条关键边,则关键边的总数为O(VE),广度优先搜索的每次迭代代价是O(E),则总时间为O(V*E^2)。
最大二分匹配。
推送-重贴标签算法。由源点向外流动,初始化时用最大流量将与源点直接相连的结点充满超额流,然后重贴高度标签,升高有残余网络的溢出结点的高度,使超额流量向高度低的结点扩散,如此反复执行溢出与标签操作,直至所有除源点与汇点外的结点都无超额流。则得到最大流。
习题26.1-4 标量流积问题,证明流为凸集。对于任意的流f1及f2,假设f3= f1*a+f2*(1- a)。首先f3要满足流量守恒,∑f(u, v)= ∑f(v, u),易知f3满足流量守恒。其次要满足容量限制。f3= f2+ a*(f1- f2),f3<= max(f1, f2),则f3(u, v)<=c(u, v)。则f3也是一个流。
习题26.1-5 最大流问题表述为线性规划问题。f(u, v)为u到v路径上的流量,各项有最大值c,f(u, v)<= c(u, v),各结点流量守恒,∑f(u, v)= ∑f(v, u)。变换为一组方程组。
习题26.1-7 结点容量问题。一个结点分割为两个结点,彼此之间的最大流量设定为结点容量即可。
习题26.2-6 设置超级源点与超级汇点。超级源点到每个源结点的路径最大容量是pi,而每个汇点到超级汇点的路径最大容量是qi。
习题26.2-11 最大流算法确定无向图的边连通性。因为最大流等于最小切割容量,设置所有边容量为1,最大流为n,则源点与汇点之间最少有n条通路,需要删除n条边才能保证图不连通。找到一个结点与其余每个结点间最大流,则最大流的最大值就是连通值。
习题26.2-12 因为f(v, s)= 1,根据流量守恒,f中一定有另外一条路径由s到v,残存网络有一条至少容量为1由s经过v回到s的路径。则存在f’,将此路径与f叠加,使得f‘中v到s流量为0. 找到此路径的方法是在残存网络中用BFS算法寻找v到s容量至少为1的路径。
习题26.4-4 快速算法找到最小切割。残存网络的边与最大流量大小相当方向相反,则分别放入最小切割的两侧,然后分别向源点与汇点追溯,各自加入两个集合。
习题26.4-8 证明Generic-push-relabel算法维持u.h < |V|意味着u.h <= δf(u, t),维持u.h>=|V|意味着u.h-|V| <= δf(u, s)。因为残存网络中u到v的距离为δf(u, v),则可知u.h <= v.h+δf(u, v)。若维持u.h < |V|,又有t.h=0,则结点u满足u.h <= δf(u, t)。若维持u.h >= |V|,又有s.h = |V|,则满足u.h - |V| <= δf(u, s)。
思考题26-1 逃逸问题。证明在结点和边都有容量的流网络中确定最大流的问题可以规约为同等规模网络中的普通最大流问题。每个结点前放置一个结点,结点对设定为一个只能进,一个只能出,此二结点之间流量限制为结点容量,则与原问题等价。解决逃逸问题也是同理。每个结点增加一个伴随结点,一只能进一只能出,结点之间容量为1,可按照一般的最大流算法解。
思考题26-2 最小路径覆盖问题。假定V={1, 2, … n},构建图G’=(V’, E’),其中V’= {x0, x1, … xn} ∪ {y0, y1, … yn},E’= { (x0, xi) } ∪ { (yi, y0) } ∪ { (xi, yj) },所有边容量为1。在图中运行最大流算法,得到由源点x0的直接流入点 Vin = {xi: (x0, xi)在最大流中},与汇点y0的直接流出点 Vout = {yj: (yj, y0)在最大流中},两个集合的元素数量应该相等,并且等于最小路径覆盖的数量,在最大流中从Vin出发,按照深度遍历到达Vout,得到全部路径。带环路的情况则需要修改算法,Vin与Vout的数量不等于最小路径覆盖的数量,而是需要从Vin出发深度遍历到达Vout,统计出全部路径。
思考题26-3 算法咨询。对于有限容量的切割(S, T),有Ji ∈ T,则对于每个结点Ak ∈ Ri,有Ak ∈ T。令Ay集合表示聘请专家的子领域,An集合表示不聘请,Jy集合表示接受的工作,Jn集合表示不接受的工作。则∑Py - ∑Cy为利润。在图中,最小切割的容量不可能包含有A到J边,因为这些边的容量都是无穷大。故属于同一子集的A与J必然在切割的同一边。因为∑Py - ∑Cy是利润,而∑Py =∑P - ∑Pn,可知利率最大时,∑Cy + ∑Pn为最小。也恰好是最小切割容量。根据最大流最小切割定理,用S到T的最大流算法可以解决。用全部工作的总营收值∑P 减去最小切割容量,即可得到最大利润。

    推荐阅读