深度优先生成树及其应用

摘要:
I无向图的深度第一生成树。以下是一个无向图及其对应的深度优先生成树:不难发现树的预序遍历过程是DFS过程,我们可以通过使用树更好地理解DFS。对于无向图,深度优先生成树的一个重要应用是解决对偶连通性问题。

在上一篇博客判断有向图是否有圈中从递归的角度简单感性的介绍了如何修改深度优先搜索来判断一个有向图是否有圈。事实上, 它的实质是利用了深度优先生成树(depth-first spanning tree)的性质。那么什么是深度优先生成树?顾名思义,这颗树由深度优先搜索而生成的,由于无向图与有向图的深度优先生成树有差别,下面将分别介绍。

一. 无向图的深度优先生成树

无向图的深度优先生成树的生成步骤:

  1. 深度优先搜索第一个被访问的顶点为该树的根结点。
  2. 对于顶点v,其相邻的边w如果未被访问,则边(v, w)为该树的树边,用实线表示;若w已经被访问,则边(v, w)为该树的回退边(back edge),用虚线表示(代表这条边实际上不是树的一部分)。

下面是一个无向图和它对应的深度优先生成树:

深度优先生成树及其应用第1张深度优先生成树及其应用第2张

不难发现,该树的先序遍历过程就是DFS过程,利用该树我们可以更好的理解DFS。而对无向图而言,深度优先生成树一个重要的应用是解决

双连通性问题(该问题在通讯网络,运输网络等有重要应用)。当然,我们首先需要了解双连通性问题的相关概念。

  1. 如果一个连通的无向图中的任一顶点被删除后,剩下的图仍然连通,那么这样的无向连通图就称作是双连通的(biconnected)。(上图的无向图是双连通的)
  2. 如果一个图不是双连通的,也就是说存在一些顶点,将其删除后图将不在连通,我们把那些顶点称为割点或者关节点(articulation point)

下图是一个不是双连通的图,其中顶点C和D为割点。

深度优先生成树及其应用第3张

利用深度优先生成树求连通图中的所有割点算法如下:

  1. 通过先序遍历深度优先生成树获得每个顶点的先序编号(也是深度优先编号),不妨把顶点v的先序编号记为num(v);
  2. 计算深度优先生成树上的每一个顶点的最小编号,所谓最小编号是取顶点v和w的先序编号的较小者,其中的w是从v点沿着零条或多条树边到v的后代x(可能是v本身),以及可能沿着任意一条回退边(x,w)所能达到w的所有顶点,记为low(v)。由low(v)的定义可知low(v)是:(1).num(v);(2).所有回退边(v, w)中的最小num(w);(3).所有树边(v, w)中的最小low(w)三者中的最小值。由(3)可知我们必须先求出v的所有孩子的最小编号,故需要用后序遍历计算low(v)。
  3. 求出所有割点:
    1. 第一类割点:根节点是割点当且仅当他有两个或两个以上的孩子。因为如果根节点有多个孩子时,删除根使得其他的节点分布在不同的子树上,而每一棵子树就对应一个连通图,所以整个图就不连通了;而但根只有一个孩子时,删除它还是只有一棵子树。
    2. 第二类割点:对于除根节点以外的节点v,它是割点当且仅当它有某个孩子使得low(w) >= num(v),即以v为根节点的子树中的所有节点均没有指向v的祖先的背向边,这样若删除v,其子树就和其他部分分开了。(注意:节点v一定不是叶节点因为删除叶节点还是一棵树,而根节点之所有单独拿出来是因为任何情况下若v为根节点,一定满足low(w) >= num(v),因为num(v)是最小先序编号)。

下面是分别从A与C开始遍历上图生成的树:

深度优先生成树及其应用第4张深度优先生成树及其应用第5张

c++实现代码如下:

/*数据结构:邻接表存储图
程序说明:为简单起见,设节点的类型为整型,设visited[],num[].low[],parent[]为全局变量,
为求得先序编号num[],设置全局变量counter并初始化为1。为便于单独处理根节点设置root变量。
*/#include <cstdio>#include <vector>#include <algorithm>

using namespacestd;

const int MAX_N = 100;
vector<int>graph[MAX_N];
vector<int>artPoint;
intnum[MAX_N], low[MAX_N], parent[MAX_N];
int counter = 1;
introot;
boolvisited[MAX_N];

void Init();           //初始化图
void FindArt(int v);   //找到第二类割点
void PrintArtPoint();  //打印所有割点(第一类割点在此单独处理)

intmain()
{
    Init();
    FindArt(root);
    PrintArtPoint();
    return 0;
}

voidPrintArtPoint()
{
    int rootChild = 0;  //根节点的孩子个数
    for (int i = 0; i < graph[root].size(); i++) //计算根节点的孩子个数
{
        if (parent[graph[root][i]] ==root)
            rootChild++;
    }
    if (rootChild > 1)            //根节点孩子个数大于1则为割点
artPoint.push_back(root);
    for (int i = 0; i < artPoint.size(); i++)
        printf("%d
", artPoint[i]);
}

voidInit()
{
    inta, b;
    root = 1;
    while (scanf("%d%d", &a, &b) !=EOF)
    {
        graph[a].push_back(b);
        graph[b].push_back(a);
        visited[a] = false;
        visited[b] = false;
    }
}
void FindArt(intv)
{
    visited[v] = true;
    low[v] = num[v] = counter++;          //情况(1)
    for (int i = 0; i < graph[v].size(); i++)
    {
        int w =graph[v][i];
        if (!visited[w])           //树边
{
            parent[w] =v;
            FindArt(w);
            if (low[w] >= num[v] && v !=root)
                artPoint.push_back(v);
            low[v] = min(low[v], low[w]);  //情况(3)
}
        else if (parent[v] != w)           //回退边
{
            low[v] = min(low[v], num[w]);  //情况(2)
}
    }
}

测试运行结果如下:

深度优先生成树及其应用第6张深度优先生成树及其应用第7张

二. 有向图的深度优先生成树

我们知道有向图同样可以和无向图一样进行深度优先搜索。但是,由于有向图的特点:边的方向性导致即使两个顶点有边相连也不一定是可达的,有向图的深度优先生成树的边有了更多的情况,包括树边(tree edges), 回退边(back edges),向前边(forward edges), 横边(cross edges)。其中后三者是树实际不存在的边,通向的是已经被访问过的点。下面用一张图来直观感受一下这几种情况:

深度优先生成树及其应用第8张

事实上,有以下结论(其中num[]保存的是树节点的先序序列即DFS序列):

1、若num[v] < num[w],即v在w之后被访问,则(v,w)是树边或向前边;

此时,若visited[v]= true, visited[w] = false,(v,w)为 树边;

若visited[v]= true, visited[w] = true,(v,w) 为 向前边;比如上图的第2种情况,访问到节点3时,节点1已经被访问,且num[1]<num[3],故边(1, 3)是向前边。

2、若num[v] > num[w],即v在w之后被访问,故visited[v] = true则visited[w] = true,则(v,w)是回退边或横边;

当产生树边(i,j) 时,同时记下j的父节点:parent[j] = i, 于是对图中任一条边(v,w),由结点v沿着树边向上(parent中)查找w(可能直到根);

若找到w,则(v,w)是回退边,否则是横边。比如上图第一种情况parent[3] = 1,故边(3, 1)为回退边,而第3种情况节点3无父节点,故为横边。

到此我们就知道了如下法则:一个有向图是无圈图当且仅当它没有回退边。

查找强连通分量(SCC: Strong Connected Components)

有向图的深度优先生成树除了可以用于判断有向图是否有边,还可以用来查找强连通分量。首先给出相关概念:

强连通图:一个有向图中任意两个顶点是可以互达的。

强连通分量:对于一个非强连通图,我们可得到顶点的一些子集,使得它们到自身是强连通的。

查找强连通分量的算法:

1. Kosaraju-Sharir算法

  1. 首先对输入的图G进行一次DFS:后序遍历深度优先生成森林,将图G的顶点标号。然后将图G所有边反向,得到Gr。
  2. 每次在图Gr中还未访问的顶点中从编号最大的顶点开始对Gr进行DFS,每进行一次DFS得到的深度优先生成树中的所有顶点就是一个强连通分量;如此直到所有点被访问。

理解该算法:如果两个顶点v和w都在一个强连通分支中,则原图G中就存在v到w和w到v的路径,所以Gr也存在。 而两个顶点互达与这两个顶点在Gr中 的同一棵深度优先生成树等价。所以步骤2每次DFS都能得到一个强连通分量。

代码如下:

/*数据结构;邻接表
程序说明:1. 每对Gr进行一次DFS,生成一个强连通分量,topSort++,
          所以topSort相同的顶点即在同一个强连通分量中。
        2. 为便于得到最大编号对应的顶点,设置node[],其下标为后序编号,值为对应顶点
*/#include <cstdio>#include <vector>#include <cstring>#include <cstdlib>
using namespacestd;
const int MAX_N = 100;
vector<int> G[MAX_N];  //原图
vector<int> Gr[MAX_N]; //反转后的图
vector<int> topSort[MAX_N];    //下标为所属强分支的拓扑序
int counter = 0;       //用于编号
int node[MAX_N];       //后序遍历标号,下标为编号
boolvisited[MAX_N];
int vNum;              //图的顶点数
void DFS(intv);
void RDFS(int v, int k);  //参数k为v所在的强连通分量的拓扑序
int SCC();                //返回强连通分量的个数
void Init();              //初始化图G和Gr
intmain()
{
    Init();
    int sccNum =SCC();
    printf("%d
", sccNum);
    for (int i = 0; i < sccNum; i++)
    {
        intj;
        printf("{");
        for (j = 0; j < topSort[i].size()-1; j++)
            printf("%d, ", topSort[i][j]);
        printf("%d}
", topSort[i][j]);
    }
    return 0;
}
voidInit()
{
    scanf("%d", &vNum);
    intu, v;
    while (scanf("%d%d", &u, &v) !=EOF)
    {
        G[u].push_back(v);
        Gr[v].push_back(u);   //反向
}
}
void DFS(intv)
{
    visited[v] = true;
    for (int i = 0; i < G[v].size(); i++)
    {
        if (!visited[G[v][i]])
            DFS(G[v][i]);
    }
    node[counter++] = v;     //后序遍历
}
void RDFS(int v, intk)
{
    visited[v] = true;
    topSort[k].push_back(v);         //将属于同一强连通分量放一起
    for (int i = 0; i < Gr[v].size(); i++)
    {
        if (!visited[Gr[v][i]])
            RDFS(Gr[v][i], k);
    }
}
intSCC()
{
    memset(visited, false, sizeof(visited));
    for (int v = 1; v <= vNum; v++)
    {
        if (!visited[v])
            DFS(v);
    }
    memset(visited, false, sizeof(visited));
    int k = 0;           //初始化第一个强连通分量的拓扑序为1
    for (int i = --counter; i >= 0; i--) //从编号最大开始
{
        if (!visited[node[i]])
            RDFS(node[i], k++);
    }
    returnk;
}

测试运行结果:

深度优先生成树及其应用第9张深度优先生成树及其应用第10张

2. Tarjan算法

Tarjan算法和上文所说的双连通性问题的算法非常相似。它也是通过求出深度优先生成树的先序编号num[]和low[]。利用的性质是当num[v] == low[v]时,则以v为根节点的深度优先生成树中所有的节点为一个强连通分量,而为了获得强连通分量,我们需要用一个栈来记录。

Tarjan算法的伪码描述如下:

Tarjan(u)
{
    num[u]=low[u] = counter              //情况(1)
    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]) //情况(3)
        else if (v in Stack)             //如果节点v还在栈内
            Low[u] = min(low[u], num[v]) //情况(2)
    if (num[u] == low[u])                //如果节点u是强连通分量的根
repeat
            v = Stack.pop                //将v退栈,为该强连通分量中一个顶点
print v
        until (u==v)
}

c++代码:

/*数据结构:邻接表存储图
*/#include <cstdio>#include <vector>#include <algorithm>#include <stack>#include <cstring>

using namespacestd;

const int MAX_N = 100;
vector<int>graph[MAX_N];
vector<int> topSort[MAX_N];    //下标为所属强分支的拓扑序
stack<int>scc;
intnum[MAX_N], low[MAX_N];
int counter = 1;
int numSCC = 0;          //强连通分量个数
int vNum;                //顶点个数
bool inStack[MAX_N];     //判断顶点是否在栈中
boolvisited[MAX_N];

void Init();          //初始化图
void Tarjan(int v);   //tarjan算法查找SCC
void PrintSCC();      //打印所有SCC
voidSCC();

intmain()
{
    Init();
    SCC();
    PrintSCC();
    return 0;
}

voidSCC()
{
    memset(visited, false, sizeof(visited));
    for (int i = 1; i <= vNum; i++)
    {
        if (!visited[i])
            Tarjan(i);
    }
}

voidPrintSCC()
{
    for (int i = 0; i < numSCC; i++)
    {
        intj;
        printf("{");
        for (j = 0; j < topSort[i].size() - 1; j++)
            printf("%d, ", topSort[i][j]);
        printf("%d}
", topSort[i][j]);
    }
}

voidInit()
{
    intu, v;
    scanf("%d", &vNum);
    while (scanf("%d%d", &u, &v) !=EOF)
    {
        graph[u].push_back(v);
    }
}
void Tarjan(intv)
{
    low[v] = num[v] = ++counter;          //情况(1)
    inStack[v] = true;
    visited[v] = true;
    scc.push(v);
    for (int i = 0; i < graph[v].size(); i++)
    {
        int w =graph[v][i];
        if (!visited[w])
        {
            Tarjan(w);
            low[v] = min(low[v], low[w]);  //情况(3)
}
        else if(inStack[w])
        {
            low[v] = min(low[v], num[w]);  //情况(2)
}
    }
    if (num[v] ==low[v])
    {
        intw;
        do{
            w =scc.top();
            scc.pop();
            inStack[w] = false;
            topSort[numSCC].push_back(w);
        } while (w !=v);
        numSCC++;
    }
}
View Code

参考资料:《数据结构与算法分析-C语言描述》

《挑战程序设计竞赛》

博客:https://www.byvoid.com/blog/scc-tarjan/

免责声明:文章转载自《深度优先生成树及其应用》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇svn commit is not under version control 和 git常用指令firfox中"content-disposition", "attachment;filename=“+filename不能显示文件名字下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Tornado 【简述】

前言 python 旗下,群英荟萃,豪杰并起。单是用于 web 开发的,就有 webpy、web2py、bottle、pyramid、zope2、flask、tornado、django 等等,不一而足。最近几年较为流行的,大概也就是flask、tornado 和 django 了。 关于以上各个 web 开发框架的性能比较,上网一搜,铺天盖地——这不是...

poj 2942 Knights of the Round Table(无向图的双连通分量+二分图判定)

#include<cstdio> #include<cstring> #include<cmath> #include<cstdlib> #include<iostream> #include<algorithm> #include<vector>...

[Linux环境编程] TCP通信与多线程编程实现“多人在线聊天室”

[linux环境编程] TCP通信与多线程编程实现“多人在线聊天室” 一、基本概念1、TCP通信   TCP(Transmission Control Protocol)就是传输控制通讯协议,是TCP/IP体系结构中最主要的传输协议。其“三次握手”提供了可靠的传送,高可靠性保证了数据传输不会出现丢失与乱序,再加之TCP连接两端设有缓存用来临时存放双向通信的...

POJ 3320 Jessica‘s Reading Problem(哈希、尺取法)

http://poj.org/problem?id=3320 题意:给出一串数字,要求包含所有数字的最短长度。 思路: 哈希一直不是很会用,这道题也是参考了别人的代码,想了很久。 1 #include<iostream> 2 #include<algorithm> 3 #include<string> 4 #in...

Oracle之物化视图

来源于:http://www.cnblogs.com/Ronger/archive/2012/03/28/2420962.html 近期根据项目业务需要对oracle的物化视图有所接触,在网上搜寻关于这方面的资料,便于提高,整理内容如下: 物化视图是一种特殊的物理表,“物化”(Materialized)视图是相对普通视图而言的。普通视图是虚拟表,应用的局限...

频域特征-Fbank

Fbank是一种前端处理方法,以类似人耳的方式对音频进行处理,可以提高语音识别的性能。fbank的计算流程与语谱图类似,唯一的区别就在于加了个Mel滤波器,从而使得得到的特征更逼近人耳特性。有关于Mel滤波器的相关内容可以查阅https://mp.weixin.qq.com/s/pGwO_27x8ddQF55wTSQlmA。接下来就介绍一下fbank的求取...