图论(三):DFS的应用——拓扑排序与强连通分量

本节介绍如何使用DFS对有向无环图进行拓扑排序,以及求强连通分量的算法。

目录

一 拓扑排序

二 拓扑排序的实现

 三 强连通分量

参考


一 拓扑排序

        什么是拓扑排序呢?对于一个有向无环图G=(V,E),拓扑排序是G中所有结点的一种线性次序,满足:如果图G包含边(u,v),则结点u在拓扑排序中处于结点v的前面。拓扑排序可以理解为一系列要处理的事件的先后的顺序。边(u,v)代表完成v必须先完成u。

        注意的是:如果图G包含环路,则不可能排出一个线性次序。下图中,图a表示一个有向无环图,图b表示该图的拓扑序。

图论(三):DFS的应用——拓扑排序与强连通分量_第1张图片

         实现拓扑排序,我们先对图G进行一次DFS,这样我们能够得到每个结点的结束遍历时间     f[ ]。(详情见图论二),我们按照每个节点的f从大到小进行排序就可以得到图G的拓扑序了。

         定理正确性:我们按照f对结点进行排序,只需要证明对于任意一对不同的结点u,v \in V,,如果图G包含边(u,v),则v.f < u.f。这样排序时,u结点自然排在v结点的前面。

        当任意一条边(u,v)被DFS遍历时,v节点的颜色不可能是灰色的(节点v将是节点u的祖先,而u-v将是一条后向边,图G产生环,与条件不符)。

  • 当结点v是白色:根据白色路径定理,v将成为u的后代,则v.f
  • 当结点v是黑色:则对v全部的遍历已经完成,v.f已被设置,而u的深度优先搜索还没有完成,因此v.f

        伪代码如下:

图论(三):DFS的应用——拓扑排序与强连通分量_第2张图片

         该算法的时间复杂度为\Theta(V+E),DFS花费\Theta(V+E),链表插入操作花费O(1),共有V个节点,因此复杂度还是 \Theta(V+E)

二 拓扑排序的实现

        图以邻接链表的方式实现

//
// Created by HP on 2021/10/25.
//
#include 
#include 
#include 
using namespace std;

typedef enum {WHITE,GRAY,BLACK} color;//白色代表未被发现;灰色代表该节点已被发现但是该节点没有寻找完;黑色代表已经处理完
typedef vector> Graph;
int time;//DFS的全局时间

int Max(int a[],int len){
    int max = a[1];
    int index = 1;
    for(int i=1;i=max){
            max = a[i];
            index = i;
        }
    }
    a[index] = 0;
    return index;
}

void DFS_VISIT(int node,Graph g,int d[],int f[],int p[],int c[]){
    c[node] = GRAY;//正在DFS
    d[node] = time++;//全局时间+1
    for(int adjacency_vertex_seq:g[node]){//遍历所有邻居节点
        if(c[adjacency_vertex_seq] == WHITE){
            p[adjacency_vertex_seq] = node;//设置子节点的前驱结点
            DFS_VISIT(adjacency_vertex_seq,g,d,f,p,c);
        }
    }
    c[node] = BLACK;//该节点结束遍历
    f[node] = time++;
}

/**
 * 图的宽度优先搜索
 * @param g 图结构,邻接链表实现
 * @param d 一个节点被发现的时间,时间单位为1
 * @param f 一个节点及其子节点处理结束的时间
 * @param p 顶点的前驱节点
 * @param c 辅助:颜色数组
 */
void DFS(Graph g,int d[],int f[],int p[],int c[]){
    //init
    f[0] = 0;
    for(int i=1;i topologicalSort(Graph g){
    // 初始化辅助数组
    int d[g.size()],f[g.size()],p[g.size()],c[g.size()];
    // 创建vector保存拓扑序
    vector r = {};
    DFS(g,d,f,p,c);
    for(int i=1;i occupation = {};
    graph.push_back(occupation);
    vector a = {3,4};
    graph.push_back(a);
    vector b = {4};
    graph.push_back(b);
    vector c = {4,6};
    graph.push_back(c);
    vector d = {};
    graph.push_back(d);
    vector e = {};
    graph.push_back(e);
    vector f = {9};//6
    graph.push_back(f);
    vector g = {6,8};//7
    graph.push_back(g);
    vector h = {9};//8
    graph.push_back(h);
    vector i = {};//9
    graph.push_back(i);

    vector sorted = topologicalSort(graph);
    for(int i:sorted){
        cout<

         运行结果:

图论(三):DFS的应用——拓扑排序与强连通分量_第3张图片

 三 强连通分量

        有向图强连通分量:在有向图G中,如果两个顶点vi,vj 间(vi> vj)有一条从vi到vj的有向路径 ,同时还有一条从vj到vi的有向路径 ,则称两个顶点强连通(strongly connected) ,如果有向图G的每两个顶点都强连通,称G是一个强连通图有向图的极大强连通子图,称为强连通分量(strongly connected components)。

        本节要介绍的Tarjan算法是一种用来求解有向图强连通分量的线性时间的算法。Tarjan算法基于强连通分量的一个性质:任何一个强连通分量,必定是对原图进行深度优先搜索得到的子树。所以我们只需要找出每个子树(强连通分量)的根,然后从这棵子树的最底层开始一个一个拿出该强连通分量内的顶点即可。

        根与其所在连通分量不同之处在于它能够到达其所在连通分量的其他顶点,反之也可达,但不在这个连通分量的顶点到达不了这个根。Tarjan算法的思想是维护两个数组来寻找连通分量的根:

  • 数组DFN用来记录深度优先搜索访问顶点的顺序
  • 另一个数组Low则记录一个顶点能到达的最大子树的根(即一个顶点能够到达的DFN值最小的顶点)。

        当一个顶点的DFN值与Low值相等时即说明它就是一个连通分量的根。因为如果它不是强连通分量的根,那么它一定是属于另一个强连通分量,而且不是根,那么就存在包含当前顶点的到根的回路,可知Low一定会等于一个比该节点的DFN更小的值。

        如果当前节点为一个强连通分量的根,那么它的强连通分量一定是以该根为根节点的(剩下节点)子树。在深度优先遍历的时候维护一个,每次访问一个新节点,就压入栈。这样,由于当前节点是这个强连通分量中最先被压入堆栈的,那么在当前节点以后压入堆栈的并且仍在堆栈中的节点都属于这个强连通分量。

算法的伪代码如下:

tarjan(u){
  DFN[u]=Low[u]=++Index      // 为节点u设定次序编号和Low初值
  Stack.push(u)              // 将节点u压入栈中
 
  for each (u, v) in E       // 枚举每一条边
    if (v is not visted)   // 如果节点v未被访问过
           tarjan(v)       // 继续向下找
      Low[u] = min(Low[u], Low[v])   //更新这个点能指出去的最浅的时间戳
    else if (v in S)       // 如果节点u还在栈内
      Low[u] = min(Low[u], DFN[v])
 
  if (DFN[u] == Low[u])      // 如果节点u是强连通分量的根
       do{
        v = S.pop            // 将v退栈,为该强连通分量中一个顶点
    }while(u == v);
}

具体C++实现如下:

/**
 * 图的强连通分量——tarjan算法
 */
#include 
#include 
#include 
using namespace std;

typedef vector> Graph;

static int index = 0;
/**
 * 图的强连通分量——tarjan算法
 * @param g 图
 * @param node 遍历节点
 * @param DFN 保存DFS中的遍历次序
 * @param Low 保存能够到达的顶点中的最小DFN
 * @param visited DFS中有没有被遍历 初始全0
 * @param in_stack 结点在不在栈内 初始全0
 * @param s 栈,函数参数传地址
 * @param scc 强连通分量图,函数参数传地址
 */
void Tarjan(Graph g,int u,int DFN[],int Low[],int visited[],int in_stack[],
stack &s,vector> &scc){
    DFN[u] = Low[u] = ++index;//先自增再赋值
    s.push(u);
    in_stack[u] = 1;
    visited[u] = 1;
    for(int adjacency:g[u]){//g[u]得到的是序号为u节点的邻接链表(vector)
        if(visited[adjacency] == 0){
            //没有被访问过
            Tarjan(g,adjacency,DFN,Low,visited,in_stack,s,scc);
            Low[u] = min(Low[u],Low[adjacency]);
        }
        if(in_stack[adjacency] == 1){
            //在栈内,u与该节点强连通
            Low[u] = min(Low[u],DFN[adjacency]);
        }
    }
    cout< scc_item;
        int v;
        do
        {
            v = s.top();
            s.pop();
            scc_item.push_back(v);
            in_stack[v] = 0;
        } while (u != v);
        scc.push_back(scc_item);

    }


}

int main(){
    Graph graph;
    vector occupation = {};
    graph.push_back(occupation);
    vector a = {2,3};//1
    graph.push_back(a);
    vector b = {4};
    graph.push_back(b);
    vector c = {4,5};
    graph.push_back(c);
    vector d = {1,6};
    graph.push_back(d);
    vector e = {6};
    graph.push_back(e);
    vector f = {};
    graph.push_back(f);

    int DFN[7],Low[7],visited[7],in_stack[7];
    for(int i=0;i<7;i++){
        DFN[i] = Low[i] = visited[i] = in_stack[i] = 0;
    }

    stack s;
    vector> scc;
    Tarjan(graph,1,DFN,Low,visited,in_stack,s,scc);

    for(int i=0;i
图论(三):DFS的应用——拓扑排序与强连通分量_第4张图片  DFS顺序有所不同,但结果相同

        时间复杂度分析:运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(V+E)

参考

【1】[转] 强连通分量的DFS求解算法_慕课手记 (imooc.com)

【2】(1条消息) 【图论】Tarjan算法详解_360°顺滑-CSDN博客

你可能感兴趣的:(算法,图论)