第8章 图 8-1 画出1个顶点、2个顶点、3个顶点、4个顶点和5个顶点的无向完全图。试证明在n个顶点的无向完全图中,边的条数为n(n-1)/2。 【解答】 【证明】 在有n个顶点的无向完全图中,每一个顶点都有一条边与其它某一顶点相连,所以每一个顶点有n-1条边与其他n-1个顶点相连,总计n个顶点有n(n-1)条边。但在无向图中,顶点i到顶点j与顶点j到顶点i是同一条边,所以总共有n(n-1)/2条边。 8-2 右边的有向图是强连通的吗?请列出所有的简单路径。 【解答】 判断一个有向图是否强连通,要看从任一顶点出发是否能够回到该顶点。右面的有向图做不到这一点,它不是强连通的有向图。各个顶点自成强连通分量。 所谓简单路径是指该路径上没有重复的顶点。 从顶点A出发,到其他的各个顶点的简单路径有A→B,A→D→B,A→B→C,A→D→B→C,A→D,A→B→E,A→D→E,A→D→B→E,A→B→C→F→E,A→D→B→C→F→E,A→B→C→F,A→D→B→C→F。 从顶点B出发,到其他各个顶点的简单路径有B→C,B→C→F,B→E,B→C→F→E。 从顶点C出发,到其他各个顶点的简单路径有C→F,C→F→E。 从顶点D出发,到其他各个顶点的简单路径有D→B,D→B→C,D→B→C→F,D→E,D→B→E,D→B→C→F→E。 从顶点E出发,到其他各个顶点的简单路径无。 从顶点F出发,到其他各个顶点的简单路径有F→E。 8-3 给出右图的邻接矩阵、邻接表和邻接多重表表示。 【解答】 邻接矩阵 邻接表 (3) 邻接多重表(十字链表) 8-4 用邻接矩阵表示图时,若图中有1000个顶点,1000条边,则形成的邻接矩阵有多少矩阵元素?有多少非零元素?是否稀疏矩阵? 【解答】 一个图中有1000个顶点,其邻接矩阵中的矩阵元素有10002 = 1000000个。它有1000个非零元素(对于有向图)或2000个非零元素(对于无向图),因此是稀疏矩阵。 8-5 用邻接矩阵表示图时,矩阵元素的个数与顶点个数是否相关?与边的条数是否相关? 【解答】 用邻接矩阵表示图,矩阵元素的个数是顶点个数的平方,与边的条数无关。矩阵中非零元素的个数与边的条数有关。 8-6 有n个顶点的无向连通图至少有多少条边?有n个顶点的有向强连通图至少有多少条边?试举例说明。 【解答】 n个顶点的无向连通图至少有n-1条边,n个顶点的有向强连通图至少有n条边。例如:  特例情况是当n = 1时,此时至少有0条边。 8-7对于有n个顶点的无向图,采用邻接矩阵表示,如何判断以下问题: 图中有多少条边?任意两个顶点i和j之间是否有边相连?任意一个顶点的度是多少? 【解答】 用邻接矩阵表示无向图时,因为是对称矩阵,对矩阵的上三角部分或下三角部分检测一遍,统计其中的非零元素个数,就是图中的边数。如果邻接矩阵中A[i][j] 不为零,说明顶点i与顶点j之间有边相连。此外统计矩阵第i行或第i列的非零元素个数,就可得到顶点i的度数。 8-8对于如右图所示的有向图,试写出: (1) 从顶点①出发进行深度优先搜索所得到的深度优先生成树; (2) 从顶点②出发进行广度优先搜索所得到的广度优先生成树; 【解答】 (1) 以顶点①为根的深度优先生成树(不唯一):② ③ ④ ⑤ ⑥ (2) 以顶点②为根的广度优先生成树: 8-9 试扩充深度优先搜索算法,在遍历图的过程中建立生成森林的左子女-右兄弟链表。算法的首部为 void Graph::DFS ( const int v, int visited [ ], TreeNode<int> * t ) 其中,指针t指向生成森林上具有图顶点v信息的根结点。(提示:在继续按深度方向从根v的某一未访问过的邻接顶点w向下遍历之前,建立子女结点。但需要判断是作为根的第一个子女还是作为其子女的右兄弟链入生成树。) 【解答】 为建立生成森林,需要先给出建立生成树的算法,然后再在遍历图的过程中,通过一次次地调用这个算法,以建立生成森林。 te mplate<Type> void Graph<Type> :: DFS_Tree ( const int v, int visited [ ], TreeNode<Type> *t ) { //从图的顶点v出发, 深度优先遍历图, 建立以t (已在上层算法中建立)为根的生成树。 Visited[v] = 1; int first = 1; TreeNode<Type> * p, * q; int w = GetFirstNeighbor ( v ); //取第一个邻接顶点 while ( w != -1 ) { //若邻接顶点存在 if ( vosited[w] == 0 ) { //且该邻接结点未访问过 p = new TreeNode<Type> ( GetValue (w) ); //建立新的生成树结点 if ( first == 1 ) //若根*t还未链入任一子女 { t->setFirstChild ( p ); first = 0; } //新结点*p成为根*t的第一个子女 else q->setNextSibling ( p ); //否则新结点*p成为*q的下一个兄弟 q = p; //指针q总指示兄弟链最后一个结点 DFS_Tree ( w, visited, q ); //从*q向下建立子树 } w = GetNextNeighbor ( v, w ); //取顶点v排在邻接顶点w的下一个邻接顶点 } } 下一个算法用于建立以左子女-右兄弟链表为存储表示的生成森林。 template<Type> void Graph<Type> :: DFS_Forest ( Tree<Type> & T ) { //从图的顶点v出发, 深度优先遍历图, 建立以左子女-右兄弟链表表示的生成森林T。 T.root = NULL; int n = NumberOfVertices ( ); //顶点个数 TreeNode<Type> * p, * q; int * visited = new int [ n ]; //建立访问标记数组 for ( int v = 0; v < n; v++ ) visited[v] = 0; for ( v = 0; v < n; v++ ) //逐个顶点检测 if ( visited[v] == 0 ) { //若尚未访问过 p = new TreeNode<Type> ( GetValue ( v ) ); //建立新结点*p if ( T.root == NULL ) T.root = p; //原来是空的生成森林, 新结点成为根 else q-> setNextSibling ( p ); //否则新结点*p成为*q的下一个兄弟 q = p; DFS_Tree ( v, visited, p ); //建立以*p为根的生成树 } } 8-10 用邻接表表示图时,顶点个数设为n,边的条数设为e,在邻接表上执行有关图的遍历操作时,时间代价是O(n*e)?还是O(n+e)?或者是O(max(n,e))? 【解答】 在邻接表上执行图的遍历操作时,需要对邻接表中所有的边链表中的结点访问一次,还需要对所有的顶点访问一次,所以时间代价是O(n+e)。 8-11 右图是一个连通图,请画出 (1) 以顶点①为根的深度优先生成树; (2) 如果有关节点,请找出所有的关节点。 如果想把该连通图变成重连通图,至少在图中加几条边? 如何加? 【解答】 (1) 以顶点①为根的深度优先生成树: (2) 关节点为 ①,②,③,⑦,⑧ (3) 至少加四条边 (1, 10), (3, 4), (4, 5), (5, 6)。从③的子孙结点⑩到③的祖先结点①引一条边,从②的子孙结点④到根①的另一分支③引一条边,并将⑦的子孙结点⑤、⑥与结点④连结起来,可使其变为重连通图。 8-12试证明在一个有n个顶点的完全图中,生成树的数目至少有2n-1-1。 【证明】略 8-13 编写一个完整的程序,首先定义堆和并查集的结构类型和相关操作,再定义Kruskal求连通网络的最小生成树算法的实现。并以右图为例,写出求解过程中堆、并查集和最小生成树的变化。 【解答】 求解过程的第一步是对所有的边,按其权值大小建堆: 求解过程中并查集与堆的变化: 最后得到的生成树如下 完整的程序如下: #include <iostream.h> template <class Type> class MinHeap { public: enum { MaxHeapSize = 50 }; MinHeap ( int Maxsize = MaxHeapSize ); MinHeap ( Type Array[ ], int n ); void Insert ( const Type &ele ); void RemoveMin ( Type &Min ); void Output (); private: void FilterDown ( int start, int end ); void FilterUp ( int end ); Type *pHeap; int HMaxSize; int CurrentSize; }; class UFSets { public: enum { MaxUnionSize = 50 }; UFSets ( int MaxSize = MaxUnionSize ); ~UFSets () { delete [ ] m_pParent; } void Union ( int Root1, int Root2 ); int Find ( int x ); private: int m_iSize; int *m_pParent; }; class Graph { public: enum { MaxVerticesNum = 50 }; Graph( int Vertices = 0) { CurrentVertices = Vertices; InitGraph(); } void InitGraph (); void Kruskal (); int GetVerticesNum () { return CurrentVertices; } private: int Edge[MaxVerticesNum][MaxVerticesNum]; int CurrentVertices; }; class GraphEdge { public: int head, tail; int cost; int operator <= ( GraphEdge &ed ); }; GraphEdge :: operator <= ( GraphEdge &ed ) { return this->cost <= ed.cost; } UFSets :: UFSets ( int MaxSize ) { m_iSize = MaxSize; m_pParent = new int[m_iSize]; for ( int i = 0; i < m_iSize; i++ ) m_pParent[i] = -1; } void UFSets :: Union ( int Root1, int Root2 ) { m_pParent[Root2] = Root1; } int UFSets :: Find ( int x ) { while ( m_pParent[x] >= 0 ) x = m_pParent[x]; return x; } template <class Type> MinHeap<Type> :: MinHeap ( int Maxsize ) { HMaxSize = Maxsize; pHeap = new Type[HMaxSize]; CurrentSize = -1; } template <class Type> MinHeap<Type> :: MinHeap ( Type Array[], int n ) { HMaxSize = ( n < MaxHeapSize ) ? MaxHeapSize : n; pHeap = new Type[HMaxSize]; for ( int i = 0; i < n; i++ ) pHeap[i] = Array[i]; CurrentSize = n-1; int iPos = ( CurrentSize - 1 ) / 2; while ( iPos >= 0 ) { FilterDown ( iPos, CurrentSize ); iPos--; } } template <class Type> void MinHeap<Type> :: FilterDown ( int start, int end ) { int i = start, j = 2 * start + 1; Type Temp = pHeap[i]; while ( j <= end ) { if ( j < end && pHeap[j+1] <= pHeap[j] ) j++; if ( Temp <= pHeap[j] ) break; pHeap[i] = pHeap[j]; i = j; j = 2 * j + 1; } pHeap[i] = Temp; } template <class Type> void MinHeap<Type> :: FilterUp ( int end ) { int i = end, j = ( end - 1 ) / 2; Type Temp = pHeap[i]; while ( i > 0 ) { if ( pHeap[j] <= Temp ) break; pHeap[i] = pHeap[j]; i = j; j = ( j - 1 ) / 2; } pHeap[i] = Temp; } template <class Type> void MinHeap<Type> :: Insert ( const Type &ele ) { CurrentSize++; if ( CurrentSize == HMaxSize ) return; pHeap[CurrentSize] = ele; FilterUp ( CurrentSize ); } template <class Type> void MinHeap<Type> :: RemoveMin ( Type &Min ) { if ( CurrentSize < 0 ) return; Min = pHeap[0]; pHeap[0] = pHeap[CurrentSize--]; FilterDown ( 0, CurrentSize ); } template <class Type> void MinHeap<Type> :: Output ( ) { for ( int i = 0; i <= CurrentSize; i++ ) cout << pHeap[i] << " "; cout << endl; } void Graph :: InitGraph( ) { Edge[0][0] = -1; Edge[0][1] = 28; Edge[0][2] = -1; Edge[0][3] = -1; Edge[0][4] = -1; Edge[0][5] = 10; Edge[0][6] = -1; Edge[1][1] = -1; Edge[1][2] = 16; Edge[1][3] = -1; Edge[1][4] = -1; Edge[1][5] = -1; Edge[1][6] = 14; Edge[2][2] = -1; Edge[2][3] = 12; Edge[2][4] = -1; Edge[2][5] = -1; Edge[2][6] = -1; Edge[3][3] = -1; Edge[3][4] = 22; Edge[3][5] = -1; Edge[3][6] = 18; Edge[4][4] = -1; Edge[4][5] = 25; Edge[4][6] = 24; Edge[5][5] = -1; Edge[5][6] = -1; Edge[6][6] = -1; for ( int i = 1; i < 6; i++ ) for ( int j = 0; j < i; j ++ ) Edge[i][j] = Edge[j][i]; } void Graph :: Kruskal( ) { GraphEdge e; int VerticesNum = GetVerticesNum ( ); int i, j, count; MinHeap<GraphEdge> heap ( VerticesNum *VerticesNum ); UFSets set ( VerticesNum ); for ( i = 0; i < VerticesNum; i++ ) for ( j = i + 1; j < VerticesNum; j++ ) if ( Edge[i][j] > 0 ) { e.head = i; e.tail = j; e.cost = Edge[i][j]; heap.Insert ( e ); } count = 1; while ( count < VerticesNum ) { heap.RemoveMin ( e ); i = set.Find ( e.head ); j = set.Find ( e.tail ); if ( i != j ) { set.Union ( i, j ); count++; cout << "( " << e.head << ", " << e.tail << ", " << e.cost << " )" << endl; } } } 8-14 利用Dijkstra算法的思想,设计一个求最小生成树的算法。 【解答】 计算连通网络的最小生成树的Dijkstra算法可描述如下:将连通网络中所有的边以方便的次序逐步加入到初始为空的生成树的边集合T中。每次选择并加入一条边时,需要判断它是否会与先前加入T的边构成回路。如果构成了回路,则从这个回路中将权值最大的边退选。 下面以邻接矩阵作为连通网络的存储表示,并以并查集作为判断是否出现回路的工具,分析算法的执行过程。 最终得到的最小生成树为 算法的思路如下: 并查集初始化:将所有顶点置为只有一个顶点的连通分量; 检查所有的边: ⅰ)若边的两个端点i与j不在同一连通分量上(i与j在并查集中不同根),则连通之(合并); ⅱ) 若边的两个端点i与j在同一连通分量上(i与j在并查集中同根),则 在并查集中寻找离i与j最近的共同祖先结点 分别从i与j向上检测具有最大权值的边 在并查集上删除具有最大权值的边,加入新的边。 下面给出实现算法: const int MaxNum = 10000; void Graph :: Dijkstra ( ) { GraphEdge e; int VerticesNum = GetVerticesNum ( ); int i, j, p, q, k; int disJoint[VerticesNum]; //并查集 for ( i = 0; i < VerticesNum; i++ ) disJoint[i] = -1; //并查集初始化 for ( i = 0; i < VerticesNum-1; i++ ) //检查所有的边 for ( j = i + 1; j < VerticesNum; j++ ) if ( Edge[i][j] < MaxNum ) { //边存在 p = i; q = j; //判结点i与j是否在同一连通分量上 while ( disJoint[p] >= 0 ) p = disJoint[p]; while ( disJoint[q] >= 0 ) p = disJoint[q]; if ( p != q ) disJoint[j] = i; // i与j不在同一连通分量上, 连通之 } else { // i与j在同一连通分量上 p = i; //寻找离结点i与j最近的祖先结点 while ( disJoint[p] >= 0 ) { //每变动一个p, 就对q到根的路径检测一遍 q = j; while ( disJoint[q] >= 0 && disJoint[q] == disJoint[p] ) q = disJoint[q]; if ( disJoint[q] == disJoint[p] ) break; else p = disJoint[p]; } k = disJoint[p]; //结点k是i和j的最近的共同祖先 p = i; q = disJoint[p]; max = -MaxNum; //从i到k找权值最大的边(s1, s2) while ( q <= k ) { if ( Edge[q][p] > max ) { max = Edge[q][p]; s1 = p; s2 = q; } p =q; q = disJoint[p]; } p = j; q = disJoint[p]; max = -MaxNum; //从j到k找权值最大的边(t1, t2) while ( q <= k ) { if ( Edge[q][p] > max ) { max = Edge[q][p]; t1 = p; t2 = q; } p =q; q = disJoint[p]; } max = Edge[i][j]; k1 = i; k2 = j; if ( max < Edge[s1][s2] ) { max = Edge[s1][s2]; k1 = s1; k2 = s2; } if ( max < Edge[t1][t2] ) { max = Edge[t1][t2]; k1 = t1; k2 = t2; } if ( max != Edge[i][j] ) { //当Edge[i][j] == max时边不改 if ( disJoint[k1] == k2 ) disJoint[k1] = -1; else disJoint[k2] = -1; //删除权值最大的边 disJoint[j] = i; //加入新的边 Edge[j][i] = - Edge[j][i]; } } } 8-15以右图为例,按Dijkstra算法计算得到的从顶点①(A)到其它各个顶点的最短路径和最短路径长度。 【解答】 源点 终点 最短路径 最短路径长度   A  B (A,B) (A,B) (A,B) (A,B) 10 10 10 10    C (A,C) (A,C) (A,C) (A,C) 18 18 18 18    D  ( (A,B,D) (A,B,D) (A,B,D) ( 15 15 15    E  ( ( (A,B,D,E) (A,B,D,E) ( ( 17 17   8-16 在以下假设下,重写Dijkstra算法: (1) 用邻接表表示带权有向图G,其中每个边结点有3个域:邻接顶点vertex,边上的权值length和边链表的链接指针link。 (2) 用集合T = V(G) - S代替S (已找到最短路径的顶点集合),利用链表来表示集合T。 试比较新算法与原来的算法,计算时间是快了还是慢了,给出定量的比较。 【解答】 用邻接表表示的带权有向图的类定义: const int DefaultSize = 10; //缺省顶点个数 class Graph; //图的前视类定义 struct Edge { //边的定义 friend class Graph; int vertex; //边的另一顶点位置 float length; //边上的权值 Edge *link; //下一条边链指针 Edge ( ) { } //构造函数 Edge ( int num, float wh ) : vertex (num), length (wh), link (NULL) { } //构造函数 int operator < ( const Edge & E ) const { return length != E.length; } //判边上权值小否 } struct Vertex { //顶点的定义 friend class Graph; char data; //顶点的名字 Edge *adj; //边链表的头指针 } class Graph { //图的类定义 private: Vertex *NodeTable; //顶点表 (各边链表的头结点) int NumVertices; //当前顶点个数 int NumEdges; //当前边数 int GetVertexPos ( const Type vertex ); //给出顶点vertex在图中的位置 public: Graph ( int sz ); //构造函数 ~Graph ( ); //析构函数 int NumberOfVertices ( ) { return NumVertices; } //返回图的顶点数 int NumberOfEdges ( ) { return NumEdges; } //返回图的边数 char GetValue ( int i ) //取位置为i的顶点中的值 { return i >= 0 && i < NumVertices ? NodeTable[i].data : ‘ ’; } float GetWeight ( int v1, int v2 ); //返回边(v1, v2)上的权值 int GetFirstNeighbor ( int v ); //取顶点v的第一个邻接顶点 int GetNextNeighbor ( int v, int w ); //取顶点v的邻接顶点w的下一个邻接顶点 } (2) 用集合T = V(G) - S代替S (已找到最短路径的顶点集合),利用链表来表示集合T。 集合T用有序链表表示,数据域为顶点序号,链表T中的顶点都是未找到最短路径的顶点。另外设置一个数组S,其作用是记录已找到的顶点0到其他各顶点的最短路径path及最短路径长度len。 算法的主要思路是: 对数组S及链表T初始化,记录顶点0到各个顶点的初始最短路径及其长度; 扫描链表T,寻找链表T中各个顶点到顶点0的当前最短路径中长度最小者,记为u; 在邻接表中扫描第u个顶点的出边表,确定每一边的邻接顶点号k。 若顶点k的最短路径没有选中过,比较绕过顶点u到顶点k的路径长度和原来顶点0到顶点k的最短路径长度,取其小者作为从顶点0到顶点k的新的最短路径。 重复执行②、③步,直到图中所有顶点的最短路径长度都已选定为止。 算法的实现如下: const float MaxNum = 10000000; typedef struct info { //辅助数组元素: 各顶点最短路径信息 int pre; //在最短路径上前一顶点的顶点序号 float len; //当前最短路径长度 } info S [NumVertices]; //辅助数组: 最短路径数组 List<int> T; //未选定最短路径顶点链表 int i, k, u; ListNode<int> * p; T. MakeEmpty (); for ( i = 1; i < NumVertices; i++ ) { S[i].pre = 0; S[i].len = MaxNum; //辅助数组初始化 T.Locate ( i ); T.Insert( i ); //形成有序链表T } p = NodeTable[0].adj; while ( p != NULL ) { S[p->vertex].len = p->length; p = p->link; } while (1) { T.First (); //循环检测链表T if ( ! T.NextNotNull( ) ) break; //链表仅剩一个顶点, 跳出循环, 算法结束 float min = MaxNum; u = 0; while ( T.NotNull( ) ) { //链表不空, 还有剩余顶点未确定最短路径 i = T.GetData( ); //取剩余顶点号 if ( S[i].len < min ) { min = S[i].len; u = i; } //比较, 寻找最短路径长度结点u T.next ( ); } p = NodeTable[u].adj; while ( p != NULL ) { //比较绕过顶点u 到其他顶点k的路径长度 k = p->vertex; //顶点k在链表T中表示该顶点未最终选定最短路径 if ( T.Find(k) != NULL && S[u].len + p->length < S[k].len ) { s[k].len = S[u].len + p->length; S[k].pre = u; } //修改 p = p->link; } T.Find(u); T.Remove(); //在链表T中删除顶点u } 8-17 试证明:对于一个无向图G = (V, E),若G中各顶点的度均大于或等于2,则G中必有回路。 【解答】 反证法:对于一个无向图G=(V,E),若G中各顶点的度均大于或等于2,则G中没有回路。此时从某一个顶点出发,应能按拓扑有序的顺序遍历图中所有顶点。但当遍历到该顶点的另一邻接顶点时,又可能回到该顶点,没有回路的假设不成立。 8-18 设有一个有向图存储在邻接表中。试设计一个算法,按深度优先搜索策略对其进行拓扑排序。并以右图为例检验你的算法的正确性。 【解答】 (1) 利用题8-16定义的邻接表结构。 增加两个辅助数组和一个工作变量: ( 记录各顶点入度 int indegree[NumVertices]。 ( 记录各顶点访问顺序 int visited[NumVertices],初始时让visited[i] = 0, i = 1, 2, (, NumVertices。 ( 访问计数int count,初始时为0。 (2) 拓扑排序算法 void Graph :: dfs ( int visited[ ], int indegree[ ], int v, int & count ) { count++; visited[v] = count; cout << NodeTable[v].data << endl; Edge *p = NodeTable[v].adj; while ( p != NULL ) { int w = p->vertex; indegree[w]--; if ( visited[w] == 0 && indegree[w] == 0 ) dfs ( visited, indegree, w, count ); p = p->link; } } 主程序 int i, j; Edge *p; float w; cin >> NumVertices; int * visited = new int[NumVertices+1]; int * indegree = new int[NumVertices+1]; for ( i = 1; i <= NumVertices; i++ ) { NodeTable[i].adj = NULL; cin >> NodeTable[i].data; cout << endl; visited[i] = 0; indegree[i] = 0; } int count = 0; cin >> i >> j >> w; cout << endl; while ( i != 0 && j != 0 ) { p = new Edge ( j, w ); if ( p == NULL ) { cout << “存储分配失败!” << endl; exit(1); } indegree[j]++; p->link = NodeTable[i].adj; NodeTable[i].adj = p; NumEdges++; cin >> i >> j >> w; cout << endl; } for ( i = 1; i <= NumVertices; i++ ) if ( visited[i] == 0 && indegree[i] == 0 ) dfs ( visited, indegree, i, count ); if ( count < NumVertices ) cout << “排序失败!” << endl; else cout << “排序成功!” << endl; delete [ ] visited; delete [ ] indegree; 8-19 试对右图所示的AOE网络,解答下列问题。 (1) 这个工程最早可能在什么时间结束。 (2) 求每个事件的最早开始时间Ve[i]和最迟开始时间Vl[i]。 (3) 求每个活动的最早开始时间e( )和最迟开始时间l( )。 (4) 确定哪些活动是关键活动。画出由所有关键活动构成的图,指出哪些活动加速可使整个工程提前完成。 【解答】 按拓扑有序的顺序计算各个顶点的最早可能开始时间Ve和最迟允许开始时间Vl。然后再计算各个活动的最早可能开始时间e和最迟允许开始时间l,根据l - e = 0? 来确定关键活动,从而确定关键路径。  1 (  2 (  3 (  4 (  5 (  6 (   Ve  0  19  15  29  38  43   Vl  0  19  15  37  38  43   <1, 2> <1, 3> <3, 2> <2, 4> <2, 5> <3, 5> <4, 6> <5, 6>   e  0  0  15  19  19  15  29  38   l  17  0  15  27  19  27  37  38  l-e  17  0  0  8  0  12  8  0   此工程最早完成时间为43。关键路径为<1, 3><3, 2><2, 5><5, 6> 8-20 若AOE网络的每一项活动都是关键活动。令G是将该网络的边去掉方向和权后得到的无向图。 (1) 如果图中有一条边处于从开始顶点到完成顶点的每一条路径上,则仅加速该边表示的活动就能减少整个工程的工期。这样的边称为桥(bridge)。证明若从连通图中删去桥,将把图分割成两个连通分量。 (2) 编写一个时间复杂度为O(n+e)的使用邻接表表示的算法,判断连通图G中是否有桥,若有。输出这样的桥。 【解答】 (1) 反证法(略) 借助于求重连通分量的算法。如果一条边的两个端点满足下列条件之一,即为桥: ① 两个端点都是关节点; ② 一个端点是关节点,另一端点是整个图的开始点; ③ 一个端点是关节点,另一端点是整个图的完成点。