1.08k likes | 1.25k Views
第十一章 图的遍历与应用. 图的遍历. V 1. 例. V 2. V 3. V 4. V 5. V 6. V 7. V 8. 图的遍历 (Graph Traversal ). 从图中某个顶点出发游历图,访遍图中其余顶点,并且使图中的每个顶点仅被访问一次的过程. 遍历 1 : V1 V2 V4 V8 V5 V3 V6 V7. 遍历 2 : V1 V2 V3 V4 V5 V6 V7 V8. 问题. 图中可能存在回路,且图的任一顶点都可能与其它顶点相通,在访问完某个顶点之后可能会沿着某些边又回到了曾经访问过的顶点
E N D
V1 例 V2 V3 V4 V5 V6 V7 V8 图的遍历(Graph Traversal ) • 从图中某个顶点出发游历图,访遍图中其余顶点,并且使图中的每个顶点仅被访问一次的过程 遍历1:V1 V2 V4 V8 V5 V3 V6 V7 遍历2:V1 V2 V3 V4 V5 V6 V7 V8
问题 • 图中可能存在回路,且图的任一顶点都可能与其它顶点相通,在访问完某个顶点之后可能会沿着某些边又回到了曾经访问过的顶点 • 为了避免重复访问,可设置一个标志顶点是否被访问过的辅助标志 visited • 辅助数组 visited的初始状态为 0, 在图的遍历过程中, 一旦此顶点被访问, 就立即让 visited为 1, 防止它被多次访问
图的遍历分类 • 深度优先搜索 DFS (Depth First Search) • 广度优先搜索 BFS (Breadth First Search)
深度优先搜索DFS 1 2 1 2 3 3 • 深度优先搜索的示例 A B A B E E 7 7 5 4 5 4 G G D C D C 6 6 H I H I F F 8 9 8 9 前进 回退 深度优先搜索过程 深度优先生成树
DFS在访问图中某一起始顶点 v后, 由v出发, 访问它的任一邻接顶点w1; 再从 w1 出发,访问与 w1邻接但还没有访问过的顶点 w2; 然后再从 w2 出发, 进行类似的访问, …如此进行下去, 直至到达所有的邻接顶点都被访问过的顶点 u 为止。接着, 退回一步, 退到前一次刚访问过的顶点, 看是否还有其它没有被访问的邻接顶点。如果有, 则访问此顶点, 之后再从此顶点出发, 进行与前述类似的访问; 如果没有, 就再退回一步进行搜索。重复上述过程, 直到连通图中所有顶点都被访问过为止。
算法实现 • 递归实现
图的深度优先搜索算法 template<class Type> void Graph <Type> :: DFS ( ) { int * visited = newint [NumVertices]; for ( int i = 0; i < NumVertices; i++ ) visited [i] = 0;//访问数组 visited 初始化 DFS (0, visited ); //从顶点0出发开始搜索 delete [ ] visited; //释放visited }
template<class Type> void Graph <Type> :: DFS ( const int v, int visited [ ] ) { cout << GetValue (v) << ‘ ’; //访问顶点v visited[v] = 1; //顶点v作访问标记 int w = GetFirstNeighbor (v); //取v 的第一个邻接顶点w while ( w != -1 ) {//若邻接顶点w 存在 if ( !visited[w] ) DFS ( w, visited ); //若顶点w 未访问过, 递归访问顶点w w = GetNextNeighbor ( v, w ); //取顶点 v 排在 w后的下一个邻接顶点 } }
V1 V2 V3 V4 V5 V6 V7 V8 V1 例 V3 V2 V5 V6 V4 V8 V7 例子 深度遍历:V1 V2 V4 V8 V5 V6 V3 V7 深度遍历:V1 V2 V4 V8 V5 V6 V3 V7
广度优先搜索BFS • 广度优先搜索的示例 1 2 1 2 5 5 A B A B E E 4 3 7 4 3 G G D C 7 D C 6 6 H I H F I F 8 9 8 9 广度优先搜索过程 广度优先生成树
算法描述 • BFS在访问了起始顶点v 之后, 由v 出发, 依次访问v的各个未被访问过的邻接顶点 w1, w2, …, wt, 然后再顺序访问w1, w2, …, wt 的所有还未被访问过的邻接顶点。再从这些访问过的顶点出发,再访问它们的所有还未被访问过的邻接顶点,…如此做下去,直到图中所有顶点都被访问到为止。
算法实现 • 广度优先搜索是一种分层的搜索过程, 每向前走一步可能访问一批顶点, 不像深度优先搜索那样有往回退的情况。因此, 广度优先搜索不是一个递归的过程。 • 为了实现逐层访问, 算法中使用了一个队列,以记忆正在访问的这一层和上一层的顶点,以便于向下一层访问。 • 为避免重复访问, 需要一个辅助数组visited[ ],给被访问过的顶点加标记。
开始 标志数组初始化 Vi=1 Vi访问过 N BFS Y Vi=Vi+1 Vi==Vexnums N Y 结束 算法流程图
开始 BFS 访问V0,置标志 初始化队列 V0入队 Y 队列空吗 a N 访问w,置标志 队头V出队 结束 求V邻接点w w入队 N w存在吗 V下一邻接点w Y Y w访问过 N a
图的广度优先搜索算法 template<class Type> void Graph <Type> ::BFS ( int v ) { int * visited = new int[NumVertices]; for ( int i = 0; i < NumVertices; i++ ) visited[i] = 0;//visited初始化 cout << GetValue (v) << ' '; visited[v] = 1; Queue<int> q; q.EnQueue (v);//进队列 while ( !q.IsEmpty ( ) ) {//队空搜索结束 v = q.GetFront( ); q.DeQueue ( ); int w = GetFirstNeighbor (v); while ( w != -1 ) {//若邻接顶点w存在 if ( !visited[w] ) {//未访问过 cout << GetValue (w) << ‘’; visited[w] = 1; q.EnQueue (w); } w = GetNextNeighbor (v, w); }//重复检测 v的所有邻接顶点 }//外层循环,判队列空否 delete [ ] visited; }
V1 V2 V3 V4 V5 V6 V7 V8 V1 例 V3 V2 V5 V6 V4 V8 V7 例子 广度遍历:V1 V2 V3 V4 V5 V6 V7 V8 广度遍历:V1 V2 V3 V4 V5 V6 V7 V8
非连通图的连通分量 • 当无向图为非连通图时, 从图中某一顶点出发, 利用深度优先搜索算法或广度优先搜索算法不可能遍历到图中的所有顶点, 只能访问到该顶点所在的最大连通子图(连通分量)的所有顶点。
连通分量 • 若从无向图的每一个连通分量中的一个顶点出发进行遍历, 可求得无向图的所有连通分量。 • 在算法中, 需要对图的每一个顶点进行检测:若已被访问过,则该顶点一定是落在图中已求得的连通分量上;若还未被访问,则从该顶点出发遍历图,可求得图的另一个连通分量。 • 对于非连通的无向图,所有连通分量的生成树组成了非连通图的生成森林。
A H K L M N B C D I E O F G J 非连通无向图 A H K L M N B C D I E O F G J 非连通图的连通分量
确定连通分量的算法 template <class Type> void Graph <Type> :: Components ( ) { int *visited = new int[NumVertices]; for ( int i = 0; i < NumVertices; i++ ) visited[i] = 0; //visited 初始化 for ( i = 0; i < NumVertices; i++ ) if ( !visited[i] ) { //检测顶点是否访问过 DFS ( i, visited ); //未访问, 出发访问 OutputNewComponent ( ); //连通分量 } delete [ ] visited; }
A 【例】以深度优先搜索方法从顶点 出发遍历图, 建立深度优先生成森林。 A A B C B C G D E F E D F 深度优先生成森林 有向图 G
template<class Type> void Graph<Type> :: • DFS_Forest ( Tree <Type> &T ) { • TreeNode <Type> * rt, * subT; • int *visited = new int[n]; //创建访问数组 • for ( int i = 0; i < n; i++ ) • visited [i] = 0; //初始化 • for ( i = 0; i < n; i++ ) //遍历所有顶点 • if ( !visited[i] ) { //顶点i未访问过 • if ( T.IsEmpty ( ) )//原为空森林,建根 • subT = rt = T.BuildRoot( GetValue(i)); • //顶点i的值成为根rt的值 • else
subT = T.InsertRightSibling • ( subT, GetValue(i)); • //顶点i的值成为 subT右兄弟的值 • DFS_Tree ( T, subT, i, visited); • //从顶点i 出发深度优先遍历, 建立以 • // subT为根的T 的子树 • } • } • template<class Type> void Graph <Type> :: DFS_Tree ( Tree<Type> &T, TreeNode <Type>*RT, int v, int visited [ ] ) {
TreeNode<Type> * p; • visited [v] = 1; //顶点v 作访问过标志 • int w = GetFirstNeighbor (v); • //取顶点v 的第一个邻接顶点w • int FirstChild = 1; • //第一个未访问子女应是v 的左子女 • while ( w ) { //邻接顶点 w存在 • if ( ! visited [w] ) { • // w未访问过, 将成为v 的子女 • if ( FirstChild ) { • p = T.InsertLeftChild ( RT,GetValue(w)); • // p 插入为RT 的左子女
FirstChild = 0; //建右兄弟 • } • else p = T.InsertRightSibling • ( p, GetValue(w)); • // p 插入为p 的左子女 • DFS_Tree ( T, p, w, visited); • //递归建立w 的以p 为根的子树 • } //邻接顶点w 处理完 • w = GetNextNeighbor ( v, w); • //取 v 的下一个邻接顶点w • } //回到while 判邻接顶点w 存在 • }
重连通分量 (Biconnected Component) • 在无向连通图G中, 当且仅当删去G中的顶点v及所有依附于v的所有边后, 可将图分割成两个或两个以上的连通分量,则称顶点v为关节点。 • 没有关节点的连通图叫做重连通图。 • 在重连通图上, 任何一对顶点之间至少存在有两条路径, 在删去某个顶点及与该顶点相关联的边时, 也不破坏图的连通性。 • 一个连通图G如果不是重连通图,那么它可以包括几个重连通分量。
在一个无向连通图G中, 重连通分量可以利用深度优先生成树找到。 • 在图中各顶点旁标明的深度优先数, 给出进行深度优先搜索时各顶点访问的次序。 • 深度优先生成树的根是关节点的充要条件是它至少有两个子女。 • 其它顶点 u 是关节点的充要条件是它至少有一个子女 w, 从 w 出发, 不能通过 w、w 的子孙及一条回边所组成的路径到达 u 的祖先。
10 9 3 A I J A I J 2 8 B H B H 1 6 关节点 关节点 4 C D F C D F 5 7 E G E G A J I 关节点 B H 根有两 个子女 C D F E G
最小生成树 minimum cost spanning tree
最小生成树 • 使用不同的遍历图的方法,可以得到不同的生成树;从不同的顶点出发,也可能得到不同的生成树。 • 按照生成树的定义,n 个顶点的连通网络的生成树有 n 个顶点、n-1 条边。
构造最小生成树的准则 • 必须使用且仅使用该网络中的n-1 条边来联结网络中的 n 个顶点; • 不能使用产生回路的边; • 各边上的权值的总和达到最小
思考 • 如何设计算法生成最小生成树?
克鲁斯卡尔 (Kruskal) 算法 • 克鲁斯卡尔算法的基本思想: 设有一个有 n 个顶点的连通网络N = { V, E}, 最初先构造一个只有 n 个顶点, 没有边的非连通图T = { V, }, 图中每个顶点自成一个连通分量。当在 E 中选到一条具有最小权值的边时, 若该边的两个顶点落在不同的连通分量上,则将此边加入到 T 中; 否则将此边舍去,重新选择一条权值最小的边。如此重复下去, 直到所有顶点在同一个连通分量上为止。
算法的框架 利用最小堆(MinHeap)和并查集(DisjointSets)来实现克鲁斯卡尔算法。算法的框架 利用最小堆(MinHeap)和并查集(DisjointSets)来实现克鲁斯卡尔算法。 • 首先, 利用最小堆来存放E中的所有的边, 堆中每个结点的格式为 • 在构造最小生成树过程中, 利用并查集的运算检查依附一条边的两顶点tail、head是否在同一连通分量 (即并查集的同一个子集合) 上, 是则舍去这条边;否则将此边加入 T, 同时将这两个顶点放在同一个连通分量上。 tail head cost 边的两个顶点位置 边的权值
随着各边逐步加入到最小生成树的边集合中, 各连通分量也在逐步合并, 直到形成一个连通分量为止。 应用克鲁斯卡尔算法构造最小生成树的过程 28 0 1 0 1 0 1 10 10 14 16 5 6 2 5 6 2 5 6 2 18 24 25 12 4 3 4 3 4 3 22 原图 (a) (b)
28 0 1 0 1 0 1 10 10 10 14 16 14 5 6 2 5 6 2 5 6 2 18 24 25 12 12 12 4 3 4 3 4 3 22 原图 (c) (d) 0 1 0 1 0 1 10 10 10 14 16 14 16 14 16 5 6 2 5 6 2 5 6 2 12 12 25 12 4 3 4 3 4 3 22 22 (e) (f) (g)
最小生成树类定义 const intMAXNUM = 足够大的大数 class MinSpanTree; class MSTEdgeNode { //生成树边结点类 friend class MinSpanTree; private: int tail, head;//生成树各边的两顶点 float cost; //生成树各边的权值 public: MSTEdgeNode ( ) //构造函数 : tail ( -1 ), head ( -1 ), cost ( 0 ) { } };
class MinSpanTree { protected: MSTEdgeNode * edgevalue; int MaxSize, n; public: MinSpanTree ( int sz = NumVertices-1 ) : MaxSize(sz), n (0) { edgevalue = new MSTEdgeNode[MaxSize]; } int Insert ( MSTEdgeNode& item ); }
利用克鲁斯卡尔算法建立最小生成树 void Graph<string> :: Kruskal ( MinSpanTree& T ) { MSTEdgeNode e; //边结点辅助单元 MinHeap <MSTEdgeNode> H( NumEdges ); int NumVertices = VerticesList.last;//顶点数 UFSets F ( NumVertices ); //并查集 for ( int u = 0; u < NumVertices; u++ ) for ( int v = u +1; v < NumVertices; v++ ) if ( Edge[u][v] != MAXNUM ) { e.tail = u; e.head = v; //插入堆 e.cost = Edge[u][v]; H.Insert (e);
} int count = 1; //最小生成树加入边数计数 while ( count < NumVertices ) { H.RemoveMin ( e ); //从堆中退出一条边 u = F.Find ( e.tail ); //检测两端点的根 v = F.Find ( e.head ); if ( u != v ) { //根不在同一连通分量 F.Union ( u, v ); //合并 T.Insert ( e );//该边存入最小生成树 count++; } } }
出堆顺序(0,5,10)选中(2,3,12)选中 (1,6,14)选中(1,2,16)选中 (3,6,18) 舍弃 (3,4,22)选中 (4,6,24) 舍弃 (4,5,25)选中 0 1 2 3 4 5 6 0 1 2 3 4 5 6 0 28 10 0 28016 14 1 16012 2 12022 18 3 22025 24 4 10 250 5 14 18 24 0 6 F -1 -1 -1 -1 -1 -1 -1 -2 -1 -1 -1 -1 0 -1 -2 -1 -2 2 -1 0 -1 1 -2 -1 -2 -2 2 0 1 1 -2 -4 2 -1 0 1 1 -2 -5 1 2 0 -7 1 1 2 1 0 1 并查集 邻接矩阵表示
普里姆(Prim)算法 • 普里姆算法的基本思想: 从连通网络 N = { V, E }中的某一顶点 u0出发, 选择与它关联的具有最小权值的边 ( u0, v ), 将其顶点加入到生成树顶点集合U中。 以后每一步从一个顶点在 U 中,而另一个顶点不在 U 中的各条边中选择权值最小的边(u, v), 把它的顶点加入到集合 U 中。如此继续下去, 直到网络中的所有顶点都加入到生成树顶点集合 U 中为止。 • 采用邻接矩阵作为图的存储表示。
28 0 1 0 1 0 1 10 10 10 14 16 5 6 2 5 6 2 5 6 2 18 24 12 25 25 4 3 4 3 4 3 22 原图 (a) (b) 0 1 0 1 0 1 10 10 10 14 16 5 6 2 5 6 2 5 6 2 25 25 12 25 12 4 3 4 3 4 3 22 22 22 (c) (d) (e) (f)
在构造过程中,还设置了两个辅助数组: • lowcost[ ]存放生成树顶点集合内顶点到生成树外各顶点的各边上的当前最小权值; • nearvex[ ] 记录生成树顶点集合外各顶点距离集合内哪个顶点最近(即权值最小)。 28 0 1 10 14 16 5 6 2 18 24 12 25 4 3 22
0 1 2 3 4 5 6 • 若选择从顶点0出发,即u0 = 0,则两个辅助数组的初始状态为: • 然后反复做以下工作: • 在lowcost [ ]中选择 nearvex[i] -1&&lowcost[i]最小的边, 用v标记它。则选中的权值最小的边为(nearvex[v], v), 相应的权值为lowcost[v]。 0 28 10 lowcost nearvex -1 0 0 0 0 0 0
将nearvex[v] 改为-1, 表示它已加入生成树顶点集合。 • 将边(nearvex[v], v, lowcost[v] ) 加入生成树的边集合。 • 取lowcost[i] = min{ lowcost[i], Edge[v][i]},即用生成树顶点集合外各顶点 i 到刚加入该集合的新顶点 v 的距离Edge[v][i]与原来它们到生成树顶点集合中顶点的最短距离lowcost[i]做比较, 取距离近的作为这些集合外顶点到生成树顶点集合内顶点的最短距离。
如果生成树顶点集合外顶点 i 到刚加入该集合的新顶点 v 的距离比原来它到生成树顶点集合中顶点的最短距离还要近,则修改nearvex[i] : nearvex[i] = v。表示生成树外顶点i到生成树内顶点v当前距离最近。 0 1 2 3 4 5 6 0 28 10 lowcost nearvex -1 0 0 0 0 0 0 选 v=5 选边 (0,5)
顶点v=5加入生成树顶点集合: 0 1 2 3 4 5 6 0 28 2510 lowcost nearvex -1 0 0 0 5-1 0 选 v=4 选边 (5,4) 28 0 1 0 1 10 10 14 16 5 6 2 5 6 2 18 24 12 25 25 4 3 4 3 22 原图 边 (0,5,10)加入生成树