7.4图的连通性
1. 无向图的连通分量
在对图遍历时,对于连通图,无论是广度优先搜索还是深度优先搜索,仅需要调用一次搜索过程,即从任一个顶点出发,便可以遍历图中的各个顶点。对于非连通图,则需要多次调用搜索过程,而每次调用得到的顶点访问序列恰为各连通分 量中的顶点集。例如,下图(a)是一个非连通图,按照它的邻接表进行深度优先搜 索遍历,三次调用 DepthFirstSearch 过程得到的访问顶点序列为:
可以利用图的遍历过程来判断一个图是否连通。如果在遍历的过程中,不止一次调用搜索过程,则说明该图就是一个非连通图。几次调用搜索过程,表明该图就有几个连通分量。
2.图中两个顶点之间的简单路径
在图的应用问题中,常常需要找从顶点 u 到另一个顶点 v 的简单路径,即路 径中的顶点均不相同。u 到 v 可能存在多条简单路径,由于遍历过程将走遍图中 的所有顶点,故可以在深度(或广度)优先搜索算法基础上加以适当的条件,就 能得到求解此问题的算法,因此可以将此问题看成是有条件的图遍历过程。
【算法思想】
从顶点 u 开始,进行深度(或广度)优先搜索,如果能够搜索 到顶点 v,则表明从顶点 u 到顶点 v 有一条路径。由于在搜索过程中,每个顶点 只访问一次,所以这条路径必定是一条简单路径。因此,只要在搜索过程中,把 搜索的线路记录下来,并在搜索到顶点 vj 时退出搜索过程,这样就可得到从 u 到 v 的一条简单路径。
为了记录搜索线路,需要设置一个数组 pre[n],当从某个顶vi找到其邻接顶 点vj进行访问时,将 pre[j]置为 i , 即:pre[j]=i 。 这样,当退出搜索后,就能根据 pre 数组从顶点 v 追溯到顶点 u,从而输出这条从 u 到 v 的简单路径。在具体设 计这一算法时,可以顺便用 pre 数组替代 visited 数组,用 pre[j]=-1 表示 vj未被访问,pre[j]!=-1 表示 vj已被访问。
根据上面的思路,对深度优先搜索算法进行修改,就可得到一个求 u 到 v 的 简单路径的算法。
【算法描述】 深度优先找出从顶点 u 到 v 的简单路径
int *pre; void one_path(Graph *G, int u, int v) /*在连通图 G 中找一条从第 u 个顶点到 v 个顶点的简单路径*/
{
int i;
pre=(int *)malloc(G->vexnum*sizeof(int));
for(i=0;i<G->vexnum;i++) pre[i]=-1;
pre[u]=u; /*将 pre[u]置为非-1,表示第 u 个顶点已被 访问*/
DFS_path(G, u, v); /*用深度优先搜索找一条从u到v的简单路径。 */
free(pre);
}
void DFS_path(Graph *G, int u, int v) /*在连通图 G 中用深度优先搜索策略找一条从 u 到 v 的简单路径。*/
{
int j;
if(pre[v]!=-1)
for(j=firstadj(G,u);
j>=0;
j=nextadj(G,u,j))
if(pre[j]==-1)
{
pre[j]=u;
if(j==v)
print_path(pre ,v); /*输出路径*/
else
DFS_path(G,j,v);
}
}
3. 图的生成树与最小生成树
(1)生成树
一个连通图的生成树是指一个极小连通子图,它含有图中的全部顶点,但只 有足已构成一棵树的n-1条边,如图7.5所示。如果在一棵生成树上添加一条边, 必定构成一个环,这是因为该条边使得它依附的两个顶点之间有了第二条路经。 一棵有 n 个顶点的生成树有且仅有 n-1 条边,如果它多于 n-1 条边,则一定有回路。但是,有 n-1 条边的图并非一定连通,不一定存在生成树。如果一个图有 n 个顶点且边数小于 n-1 条,则该图一定是非连通图。
(2)最小生成树
在一个连通网的所有生成树中,各边的代价之和最小的那棵生成树称为该连 通网的最小代价生成树(Minimum Cost Spanning Tree),简称为最小生成树(MST)。
最小生成树有如下重要性质:
设 N=(V,{E}) 是一个连通网,U 是顶点集 V 的一个非空子集。若(u , v)是 一条具有最小权值的边,其中 u∈U,v∈V-U,则存在一棵包含边(u , v)的最小 生成树。
可以用反证法来证明这个 MST 性质:
假设不存在这样一棵包含边(u , v)的最小生成树。任取一棵最小生成树 T, 将(u , v)加入 T 中。根据树的性质,此时 T 中必形成一个包含(u , v)的回路, 且回路中必有一条边(u’ , v’)的权值大于或等于(u , v)的权值。删除(u’ , v’), 则得到一棵代价小于等于 T 的生成树 T’,且 T’为一棵包含边(u , v)的最小生成 树。这与假设矛盾,故该性质得证。 可以利用 MST 性质来生成一个连通网的最小生成树。普里姆(Prim)算法和克 鲁斯卡尔(Kruskal)算法便是利用了这个性质。
下面分别介绍这两种算法。
(1)普里姆算法
假设 N=(V,{E})是连通网,TE 为最小生成树中边的集合。
- ①初始 U={u0}(u0∈V),TE=φ;
- ②在所有 u∈U, v∈V-U 的边中选一条代价最小的边(u0,v0)并入集合 TE, 同时将 v0并入 U;
- ③重复(2), 直到 U=V 为止。
此时,TE 中必含有 n-1 条边,则 T=(V,{TE})为 N 的最小生成树。
下图给出了一个连通网,以及从顶点 V1 开始构造最小生成树的例子。可以 看出,普里姆算法逐步增加 U 的中顶点,可称为“加点法”。
注意:选择最小边时,可能有多条同样权值的边可选,此时任选其一。
为了实现这个算法需要设置一个辅助数组 closedge[ ],以记录从 U 到 V-U 具 有最小代价的边。
对每个顶点 v∈V-U,closedge[v]记录所有与 v 邻接的、从 U 到 V-U 的那组边 中的最小边信息。closedge[v]包括两个域:adjvex 和 lowcost,其中 lowcost 存储 最小边的权值,adjvex 记录最小边在 U 中的那个顶点。
显然有 closedge[v].lowcost=Min({cost(u,v) | u∈U})
【算法思想】
- ①首先将初始顶点 u 加入 U 中,对其余的每一个顶点 i,将 closedge[i]均初始化为到 u 的边信息;
- ②循环 n-1 次,做如下处理:
a) 从各组最小边 closedge[v]中选出最小的最小边 closedge[k0];( v, k0∈V-U)
b) 将 k0 加入 U 中;
c) 更新剩余的每组最小边信息 closedge[v]:( v∈V-U)
对于以 v 为中心的那组边,新增加了一条从 k0 到 v 边,如果新边的权值比 closedge[v].lowcost 小,则将 closedge[v].lowcost 更新为新边的权值。
【算法描述】 普里姆算法
struct
{
VertexData adjvex;
int lowcost;
}closedge[MAX_VERTEX_NUM]; /* 求最小生成树时的辅助数组*/
MiniSpanTree_Prim(AdjMatrix gn, VertexData u) /*从顶点 u 出发,按普里姆算法构造连通网 gn 的最小生成树,并输出生成树的 每条边*/
{
k=LocateVertex(gn, u);
closedge[k].lowcost=0; /*初始化,U={u} */
for (i=0; i<gn.vexnum; i++)
if ( i!=k) /*对 V-U 中的顶点 i,初始化 closedge[i]*/
{
closedge[i].adjvex=u;
closedge[i].lowcost=gn.arcs[k][i].adj;
}
for (e=1; e<=gn.vexnum-1; e++) /*找 n-1 条边(n= gn.vexnum) */
{
k0=Minium(closedge); /* closedge[k0]中存有当前最小边(u0,v0)的 信息*/
u0= closedge[k0].adjvex; /* u0∈U*/
v0= gn.vertex[k0] /* v0∈V-U*/
printf(u0, v0); /*输出生成树的当前最小边(u0,v0)*/
closedge[k0].lowcost=0; /*将顶点 v0 纳入 U 集合*/
for ( i=0 ;i<vexnum;i++) /*在顶点 v0 并入 U 之后,更新 closedge[i]*/
if ( gn.arcs[k0][i].adj <closedge[i].lowcost)
{
closedge[i].lowcost= gn.arcs[k0][i].adj;
closedge[i].adjvex=v0;
}
}
}
由于算法中有两个 for 循环嵌套,故它的时间复杂度为 O(n²)。
(2)克鲁斯卡尔算法
假设 N=(V,{E})是连通网,将 N 中的边按权值从小到大的顺序排列;
- ① 将 n 个顶点看成 n 个集合;
- ② 按权值由小到大的顺序选择边,所选边应满足两个顶点不在同一个顶点集 合内,将该边放到生成树边的集合中。同时将该边的两个顶点所在的顶点 集合合并;
- ③ 重复②直到所有的顶点都在同一个顶点集合内。
可以看出,克鲁斯卡尔算法逐步增加生成树的边,与普利姆算法相比,可称 为“加边法”。
以下图(a)中的连通网,详细说明克鲁斯卡尔算法的执行过程。
设 N= (V, {E}),最小生成树的初态为 T=(V, { })。
- (1)待选的边: (2,3)->5 , (2,4)->6 , (3,4)->6 , (2,6)->11 , (4,6)->14 , (1,2)->16 , (4,5)->18 , (1,5)->19, (1,6)->21 , (5,6)->33。 顶点集合状态:{1},{2},{3},{4},{5},{6} 最小生成树的边的集合: { }。
- (2)从待选边中选一条权值最小的边为: (2,3)->5。
待选的边变为:(2,4)->6 , (3,4)->6 , (2,6)->11 , (4,6)->14 , (1,2)->16 , (4,5)->18 , (1,5)->19, (1,6)->21 , (5,6)->33。
顶点集合状态变为: {1},{2,3},{4},{5},{6}。
最小生成树的边的集合:{(2,3)}。 - (3)从待选边中选一条权值最小的边为: (2,4)->6;
待选的边变为:(3,4)->6 , (2,6)->11 , (4,6)->14 , (1,2)->16 , (4,5)->18 , (1,5)->19, (1,6)->21 , (5,6)->33。
顶点集合状态变为:{1},{2,3,4},{5},{6}。
最小生成树的边的集合 {(2,3),(2,4)}。 - (4)从待选边中选一条权值最小的边为:(3,4)->6,由于 3、4 在同一个顶点 集合{2,3,4}内,故放弃。
重新从待选边中选一条权值最小的边 为:(2,6)->11。
待选的边变为:(4,6)->14 , (1,2)->16 , (4,5)->18 , (1,5)->19, (1,6)->21 , (5,6)->33。
顶点集合状态变为: {1},{2,3,4,6},{5}。
最小生成树的边的集合 {(2,3),(2,4),(2,6)}。 - (5)从待选边中选一条权值最小的边为:(4,6)->14,由于 4、6 在同一个顶点 集合{2, 3, 4, 6}内,故放弃。重新从待选边中选一条权值最小的边为:(1,2)->16。
待选的边变为:(4,5)->18 , (1,5)->19, (1,6)->21 , (5,6)->33。
顶点集合状态变为: {1,2,3,4,6},{5}。
最小生成树的边的集合: {(2,3),(2,4),(2,6),(1,2)}。 - (6)从待选边中选一条权值最小的边为: (4,5)->18。
待选的边变为: (1,5)->19, (1,6)->21 , (5,6)->33。
顶点集合状态变为: {1,2,3,4,6,5}。
最小生成树的边的集合 {(2,3),(2,4),(2,6),(1,2),(4,5)}。 至此,所有的顶点都在同一个顶点集合{1,2,3,4,6,5}里,算法结束。
所得最小生成树如下图所示,其代价为:5+6+11+16+18=56。