算法导论之图的基本算法

图是一种数据结构,有关图的算法是计算机科学中基础性的算法。这个论述恰如其分。

图的基本算法包括图的表示方法和图的搜索方法。图的搜索技术是图算法领域的核心,有序地沿着图的边访问所有顶点,可以发现图的结构信息。

1、图的表示方法:

给定图G=(V,E),其中V表示图的点、E表示图的边,V[G]表示图G的点集合,E[G]表示图G的边集合。图的表示方法主要有邻接表和邻接矩阵两类,均可用于有向图和无向图。有向图,两个顶点间有方向,是单向边,而无向图两个顶点之间是双向边。

1)邻接表

当图G中|E|远小于|V|2时,即为稀疏图,适用邻接表表示。图G=(V,E)的邻接表表示由一个包含|V|个列表的数组Adj所组成,其中每个列表对应于V中的一个顶点。对于每一个u∈V,邻接表Adj[u]包含所有满足(u,v)∈E的顶点v。即Adj[u]中包含图G中所有和顶点u相邻的顶点,如果是有向图,则是包含u指向的顶点。每个邻接表中的顶点一般以任意顺序存储。

如果G是一个有向图,则邻接表的长度之和为|E|,u指向v为一条边;如果G是一个无向图,则邻接表的长度之和为2|E|,u和v双向两条边。

邻接表的存储空间为⊙(V+E),如果是图边是加权的,可以把权值w(u,v)存储在相应数组中表示。

2)邻接矩阵

当图G中|E|接近|V|2时,即为稠密图,或者需快速判断两个顶点间是否存在连接边时,适用邻接矩阵表示。图G=(V,E)的邻接矩阵表示,假定各顶点编号从1,2,…,|V|,那么G的邻接矩阵为一个|V|X|V|的矩阵A=(aij),满足:

aij=1,如果(i,j)∈E;aij=0,如果(i,j)∉E;在有向图中属于E[G]边集合是从i到j的指向。

邻接矩阵的存储空间为⊙(V2),与图中的边数无关。如果是无向图,矩阵A的转置矩阵是自己,可以只存储对角线及对角线以上部分,存储空间节省近一半。如果图边是加权的,可以在相应矩阵行列点上存储权值。如果是非加权图,存储邻接矩阵的每个元素时,可以只用一个二进制位表示,而不必用一个字的空间。

2、图的搜索方法:

1)广度优先搜索

广度优先搜索(breadth-first search,BFS)的思想核心就是沿着点搜索图构建一颗广度优先树,发现图结构信息。给定图G=(V,E)和特定源顶点S,BFS探索G的边,发现所有S可达的顶点,并计算出S到达所有顶点之间的距离(最少边数),对有向图和无向图都适用。BFS首先发现和S距离为k的所有顶点,在发现距离为k+1的其他顶点。BFS就是找出和u点存在边关系的所有顶点后才会出发选择下一个顶点。

重点描述下BFS算法,很重要一点就是源点S到所有顶点的距离也得出,且是最短路径最少边数,这个算法导论中给出了3个引理1个推论1个定理来证明。细细品味算法导论中这些证明,有助于理解算法,同时也有助于提升数学证明逻辑,也有助于理解普适性的思想,如动态规划的分治和最优解。算法导论中给出的算法,一般行文结构是:定义、算法描述、案例、性能分析、性质或正确性证明。正确性证明中就涉及到数学基础知识基本公理基础定义和归纳推理的证明方法及普适思想,反过来说,只有掌握数学才能更好设计具有优性能和正确性的计算机算法。

BFS算法的主要变量描述:

color[u],记录顶点的状态,用白色、灰色、黑色指示,未搜索到顶点为白色,搜索到但还未完全把有边关系的顶点都搜索到的为灰色,搜索到所有有边关系的顶点的黑色;

π[u],记录u顶点的父顶点;

d[u],记录源点s到顶点u的距离,最短路径最少边数;后面带权的图最短路径就不是DFS算法了。

图G(V,E)用邻接表存储。

BFS_Func(G,s){

    //第一步:初始化所有顶点,不含源顶点s

for eachvertex u∈V[G]-{s} 

        do color[u]=white

           d[u]=∞

           π[u]=null

    //第二步:初始化源顶点s

    color[s]=gray

    d[u]=0

    π[u]=null

    //第三步:初始化先进先出队列(FIFO),源顶点(白色变灰色)入队列

    Q=null

    Enqueue(Q,s)

    //第四步:DFS搜索

    while Q≠null  //还有灰色顶点

        do u=Dequeue(Q)  //出列,第一个是源顶点s

           for each v∈Adj[u]  //邻接表中u顶点存储的具有边关系的顶点

         //白色顶点才入列,确保顶点只有一次入列机会,这个就是通过color这个指示变量来满足

               do if color[v]=white 

                  then color[v]=gray //顶点变灰色,表明已搜索到,但未搜索其相关所有顶点

                       d[v]=d[u]+1  //距离加1

                       π[v]=u   //顶点v的父顶点是u

                       Enqueue(Q,v)

           color[u]=black //顶点u都搜索完成,变黑色

}

每次阅读算法导论中的算法,都感觉到精炼、清晰,相当到位。从算法中不难得出DFS的时间运行性能是线性的,为O(V+E),虽然是双层循环,但在循环中,顶点为白色只有一次机会,所以就是V[G]顶点数,而扫描所有邻接表的次数就是图的边数E[G]。

最精彩的当然还是DFS取得的距离是最短路径。这里不赘述。

2)深度优先搜索

深度优先搜索(deepth-first search,DFS)的思想核心就是沿着边搜索图构建深度优先树林。DFS产生多个源顶点,形成多颗深度树,从一个顶点出发,从邻接表中发现第一条边的相关顶点,接着探索相关顶点邻接表中的边和相关顶点,当最后一个顶点没有边时就结束,回溯父结点的新一条边开始探索。DFS构建的深度树互不相交。应该来说BFS适用于寻找最短路径,而DFS适用于发现图的结构信息,如图的边类型。

DFS算法中的括号定理,和编译原理中语法分析思想一致。DFS给出一个括号定理和一个后裔区间的嵌套,从而得出白色路径原理。定理、推论结合DFS设计的算法都很好理解。

白色路径定理:在一个有向图或无向图G=(V,E)的深度优先森林中,顶点v是顶点u的后裔,当且仅当在搜索过程中在时刻d[u]发现u时,可以从顶点u出发,经过一条完全由白色顶点组成的路径到达v。定理的证明中,采用了综合法和分析法。

——综合法是一种从题设到结论的逻辑推理方法,也就是由因导果的证明方法;

——分析法是一种从结论到题设的逻辑推理方法,也就是执果索因法的证明方法;分析法的证明路径与综合法恰恰相反。

关于括号定理和后裔区间定理以及白色路径定理,都关系到算法中很关键的一个指标变量设计,就是时间戳。这里先谈下DFS发现图结构构建深度优先森林时关于边的分类。边的分类及性质是DFS一个重要应用点。

根据在图G上进行深度优先搜索所产生的深度优先森林Gπ,可以把图的边分类四种类型:

——树边(treeedge),是深度优先森林Gπ中的边。如果顶点v是在探寻边(u,v)时被首次发现,那么(u,v)就是一条树边;

——反向边(backedge),是深度优先树中,连接顶点u到它的某一祖先顶点v的那些边。有向图中可能出现的自环也被认为是反向边;

——正向边(forwardedge)是指深度优先树中,连接顶点u到它的某个后裔v的非树边(u,v)。

——交叉边(crossedge)是其他类型的边,存在于同一颗深度优先树中的两个顶点之间,条件是其中一个顶点不是另一个顶点的祖先。交叉边也可以在不同的深度优先树的顶点之间。

这样的一个分类对着DFS算法和案例才好理解,但看表述也基本能明白。在DFS算法中,可以对搜索到的边进行分类,分类核心思想是对于每条边(u,v),当该边被第一次搜索到时,可根据所到达的顶点v的颜色:第一类白色的v顶点,表明是一条树边;第二类灰色的v顶点,表明是一条反向边;第三类黑色的v顶点,表明是一条正向边或交叉边。这里的理解关系到DFS算法中另一个重要的指标变量设计,就是颜色。对于无向图进行深度优先搜索时边的分类,引申出一个定理:在对一个无向图G进行深度优先搜索的过程中,G的每一条边要么是树边,要么是方向边。

有时对算法导论中的定理和性质证明过程理解有点头疼,不过细看下去收获很多。以这个定理的证明来说,首先要掌握无向图的性质(边的双向性质,在邻接表中使存储两次),其次要理解DFS过程中边的分类算法,才能理解这个定理,从而应用该定理。

现在介绍下DFS算法,首先描述下主要的变量:

π[v]表示顶点v的父节点;

color[v] 表示顶点v的状态,开始为白色,第一次发现为灰色,结束该点所有边的搜索为黑色;

时间戳d[v]和f[v],在搜索过程中顶点v第一次被发现时(设为灰色)所记录的时间戳为d[v],当结束检查v的邻接表时(设为黑色)所记录的时间戳为f[v];

时间戳的直在1和2|V|之间取整,对|V|个顶点中的每一个,都有一个发现时间和完成时间,d[v]

DFS_Func(G){

   //第一步初始化所有顶点

   for each vertex u∈V[G]

      do color[u]=white

         π[u]=null

         time=0;

   //第二步多源顶点出发深度搜索

   for each vertex u∈V[G]

      do if color[u]=white

           then DFS_Visit(u)

}

DFS_Visit_Func(u){

   Color[u]=gray //第一次发现,设为灰色

   time=time+1

   d[u]=time //发现事件的时间戳

   for each v∈Adj[u]  //找出边(u,v)

      do if color[v]=white //顶点v第一次发现

         then π[v]=u //u是v的父结点

               DFS_Visit_Func(v)  //沿着边找出顶点v的边

       color[u]=black  //顶点u探索结束,设为黑色

       f[u]=time+1

}

结合图示案例可以更好理解DFS算法,尤其是递归调用。DFS算法时间性能是⊙(V+E)。

深度优先搜索的应用之一:拓扑排序。

对于有向无回路图,DFS执行中使没有反向边的,这个算法导论中给出引理并证明。有向无回路图用于说明事件的先后次序,DFS后形成一个顶点序列,即是拓扑排序。对有向无回路图无反向边的证明,采用了对命题的题设和结论的反证法。

深度优先搜索的应用之二:强连通分支

有向图G=(V,E)的一个强连通分支就是一个最大顶点集合C ⊆V,对C中的每一对顶点u和v,有uàv和vàu;即顶点u和v之间是互相可达的。强连通分支是一个有向无回路图,导论中有引理并证明。

在寻找图G=(V,E)的强连通分支算法中,提到了G的转置,即GT=(V,ET),ET={(u,v):(v,u)∈E}。ET 就是有G中边改变方向所得。在给定图G的邻接表表示情况下,建立GT 需要O(V+E)时间。G和GT有着完全相同的强连通分支。导论中证明了转置后的图GT可以计算出有向图G的强连通分支。

把有向图分解成强连通分支,再运行算法,即先分治取得解再组合解。最重要的是强连通分支的定义和性质满足解组合。

你可能感兴趣的:(Algorithm,算法导论专栏)