本文可转载,转载请注明出处:www.cnblogs.com/collectionne/p/6847240.html 。本文未完,如果不在博客园(cnblogs)发现此文章,请访问以上链接查看最新文章。

前言:之前翻译过一篇英文的关于割点的文章(英文原文翻译),但是自己还有一些不明白的地方,这里就再次整理了一下。有兴趣可以点我给的两个链接。

割点的概念

无向连通图中,如果将其中一个点以及所有连接该点的边去掉,图就不再连通,那么这个点就叫做割点(cut vertex / articulation point)。

例如,在下图中,0、3是割点,因为将0和3中任意一个去掉之后,图就不再连通。如果去掉0,则图被分成1、2和3、4两个连通分量;如果去掉3,则图被分成0、1、2和4两个连通分量。

怎么求割点

直接DFS

最容易想到的方法就是依次删除每个割点,然后DFS,但这种方法效率太低,这里不做讨论。

DFS树

首先需要了解一些关于DFS树(DFS tree)的概念。以下图为例:

从点1开始搜索整个图, 对于每个点相邻的顶点,按照顶点编号从小到大搜索(也可以按其它顺序)。因此上图的搜索顺序如下:

第1步,与1相邻的点有{2, 4},选2。

第2步,与2相邻的点有{1, 3, 4},1访问过,选3。

第3步,与3相邻的点有{2, 5},2访问过,选5。

第4步,与5相邻的点有{3},访问过,退出。

退回第3步,与3相邻的点有{2, 5},都访问过,退出。

退回第2步,与2相邻的点有{1, 3, 4},1、3访问过,选4。

第5步,与4相邻的点有{1, 2},都访问过,退出。

退回第2步,与2相邻的点有{1, 3, 4},都访问过,退出。

退回第1步,与1相邻的点有{2, 4},都访问过,退出。

至此,访问结束。

把访问顶点的路径表示出来就是这样的(访问已访问过的顶点时加上删除线并不再访问,end表示与某个顶点相邻的顶点遍历完毕,{}里是与一个顶点相邻的所有顶点)。

1 {2,4}
  2 {1,3,4}
    1
    3 {2,5}
      2
      5 {3}
        3
        end
      end
    4 {1,2}
      1
      2
      end
    end
  4
  end

访问路径可以绘制成下图(绿边为访问未访问顶点时经过的边,红边为访问已访问节点是经过的边):

我们把上图称为DFS搜索树(DFS tree),上图中的绿边称为树边(tree edge),红边称为回边(back edge)。通过回边可以从一个点返回到之间访问过的顶点

你可能会有疑问,“访问已访问节点时所经过的边叫回边”,我们上面不是没有访问吗?其实是有的,但是为方便就不写了,而且遇到已访问的边(在后面的算法里)只是简单计算一下,不再继续DFS了。

注意,在上图中,如果与一个顶点相邻A的顶点B是A的父节点,不表示出来,接下来的算法遇到这种情况也不计算

Tarjan算法

可以使用Tarjan算法求割点(注意,还有一个求连通分量的算法也叫Tarjan算法,与此算法类似)。(Tarjan,全名Robert Tarjan,美国计算机科学家。)

首先选定一个根节点,从该根节点开始遍历整个图(使用DFS)。

对于根节点,判断是不是割点很简单——计算其子树数量,如果有2棵即以上的子树,就是割点。因为如果去掉这个点,这两棵子树就不能互相到达。

对于非根节点,判断是不是割点就有些麻烦了。我们维护两个数组dfn[]和low[],dfn[u]表示顶点u第几个被(首次)访问,low[u]表示顶点u及其子树中的点,通过非父子边(回边),能够回溯到的最早的点(dfn最小)的dfn值(但不能通过连接u与其父节点的边)。对于边(u, v),如果low[v]>=dfn[u],此时u就是割点。

但这里也出现一个问题:怎么计算low[u]。

假设当前顶点为u,则默认low[u]=dfn[u],即最早只能回溯到自身。

有一条边(u, v),如果v未访问过,继续DFS,DFS完之后,low[u]=min(low[u], low[v]);

如果v访问过(且u不是v的父亲),就不需要继续DFS了,一定有dfn[v]<dfn[u],low[u]=min(low[u], dfn[v])。

代码

DFS

先回忆一下怎么用DFS遍历一个图,代码如下:

bool vis[N];            // 顶点是否访问过
vector<int> g[N];  // 邻接表表示的图 // 调用dfs()前需将整个vis[]设为false
void dfs(int u)
{
vis[u] = true;
for (int v: g[u])
{
if (!vis[v])
dfs(v);
}
}

Tarjan算法

首先假设u是根节点。如果u有两棵以上的子树,则u为割点。代码:

int children = ;
for (int v: g[u])
{
if (!vis[v])
{
children++;
dfs(v); // 继续DFS
}
}
if (children >= )
// u是割点

非根节点呢?按照前面的描述,代码如下:

// 默认u不能回溯到任何前面的点
low[u] = dfn[u];
for (int v: g[u])
{
// (u, v)为树边
if (!vis[v])
{
// 设置v的父亲为u
parent[v] = u;
// 继续DFS,遍历u的子树
dfs(v);
// u子树遍历完毕,low[v]已求出,low[u]取最小值
low[u] = min(low[u], low[v]); if (low[v] >= dfn[u])
// u是割点
}
// (u, v)为回边,且v不是u的父亲
else if (v != parent[u])
low[u] = min(low[u], dfn[v]);
}

综合起来,加上一些其它部分,Tarjan算法的代码如下:

const int V = ;
int dfn[V], low[V], parent[V];
bool vis[V], ap[V];
vector<int> g[V]; void dfs(int u)
{
static int count = ;
// 子树数量
int children = ; // 默认low[u]等于dfn[u]
dfn[u] = low[u] = ++count;
vis[u] = true; // 遍历与u相邻的所有顶点
for (int v: g[u])
{
// (u, v)为树边
if (!vis[v])
{
// 递增子树数量
children++;
// 设置v的父亲为u
parent[v] = u;
// 继续DFS
dfs(v);
// DFS完毕,low[v]已求出,如果low[v]<low[u]则更新low[u]
low[u] = min(low[u], low[v]); // 如果是根节点且有两棵以上的子树则是割点
if (parent[u] == - && children >= )
cout << "Articulation point: " << u << endl;
// 如果不是根节点且low[v]>=dfn[u]则是割点
else if (parent[u] != - && low[v] >= dfn[u])
cout << "Articulation point: " << u << endl;
}
// (u, v)为回边,且v不是u的父亲
else if (v != parent[u])
low[u] = min(low[u], dfn[v]);
}
}

不过有一个问题:可能会重复输出一个割点。例如一个图里有(1, 2)、(1, 3)、(1, 4)和(1, 5)四条边(取1为根节点),发现(1, 3)时就已经输出了1,但发现(1, 4)和(1, 5)时就又输出了两遍。所以需要使用一个数组ap[]来记录割点。

还有一个可以优化的地方:我们使用vis[]来记录一个点是否访问过。但是我们想一下,不是只有访问过的点才会分配dfn吗?当然,没有访问过的顶点,dfn[]里也有值,但这里dfn[]是全局的,因此它的每个元素最初都是0。因此完全可以取消vis[]数组并把!vis[v]改成!dfn[v]。

最后一个点:下面的代码:

if (parent[u] == - && children >= )
cout << "Articulation point: " << u << endl;
else if (parent[u] != - && low[v] >= dfn[u])
cout << "Articulation point: " << u << endl;

可以合起来写成:

if (parent[u] == - && children >=  || parent[u] != - && low[v] >= dfn[u])
cout << "Articulation point: " << u << endl;

当然,还需要加上对ap[]的检查。

对Tarjan算法的详细理解

1.

Todo

对算法的详细理解

首先,“根节点有n棵子树”这句话,是说这n棵子树是独立的,没有根节点不能互相到达。因此n不一定等于与根节点相邻的顶点数。因此加入了vis[v]为false的条件,因为如果(u, v1)和(u, v2)在一棵子树里,对v1进行DFS,一定能去到v2,vis[v2]就会为true,此时就不会children++了。

对于边(u, v),如果low[v]>=dfn[u],即v即其子树能够(通过非父子边)回溯到的最早的点,最早也只能是u,要到u前面就需要u的回边或u的父子边。也就是说这时如果把u去掉,u的回边和父子边都会消失,那么v最早能够回溯到的最早的点,已经到了u后面,无法到达u前面的顶点了,此时u就是割点。

割点(Tarjan算法)的更多相关文章

  1. 洛谷3388 【模板】割点 tarjan算法

    题目描述 给出一个n个点,m条边的无向图,求图的割点. 关于割点 在无向连通图中,如果将其中一个点以及所有连接该点的边去掉,图就不再连通,那么这个点就叫做割点(cut vertex / articul ...

  2. 割点 —— Tarjan 算法

    由于对于这一块掌握的十分不好,所以在昨天做题的过程中一直困扰着我,好不容易搞懂了,写个小总结吧 qwq~ 割点 概念 在无向连通图中,如果将其中一个点以及所有连接该点的边去掉,图就不再连通,那么这个点 ...

  3. zoj 1119 / poj 1523 SPF (典型例题 求割点 Tarjan 算法)

    poj : http://poj.org/problem?id=1523 如果无向图中一个点 u 为割点 则u 或者是具有两个及以上子女的深度优先生成树的根,或者虽然不是一个根,但是它有一个子女 w, ...

  4. 割点和桥---Tarjan算法

    使用Tarjan算法求解图的割点和桥. 1.割点 主要的算法结构就是DFS,一个点是割点,当且仅当以下两种情况:         (1)该节点是根节点,且有两棵以上的子树;         (2)该节 ...

  5. tarjan算法--求无向图的割点和桥

    一.基本概念 1.桥:是存在于无向图中的这样的一条边,如果去掉这一条边,那么整张无向图会分为两部分,这样的一条边称为桥无向连通图中,如果删除某边后,图变成不连通,则称该边为桥. 2.割点:无向连通图中 ...

  6. Tarjan算法应用 (割点/桥/缩点/强连通分量/双连通分量/LCA(最近公共祖先)问题)(转载)

    Tarjan算法应用 (割点/桥/缩点/强连通分量/双连通分量/LCA(最近公共祖先)问题)(转载) 转载自:http://hi.baidu.com/lydrainbowcat/blog/item/2 ...

  7. tarjan算法(割点/割边/点连通分量/边连通分量/强连通分量)

    tarjan算法是在dfs生成一颗dfs树的时候按照访问顺序的先后,为每个结点分配一个时间戳,然后再用low[u]表示结点能访问到的最小时间戳 以上的各种应用都是在此拓展而来的. 割点:如果一个图去掉 ...

  8. Tarjan算法:求解图的割点与桥(割边)

    简介: 割边和割点的定义仅限于无向图中.我们可以通过定义以蛮力方式求解出无向图的所有割点和割边,但这样的求解方式效率低.Tarjan提出了一种快速求解的方式,通过一次DFS就求解出图中所有的割点和割边 ...

  9. 『Tarjan算法 无向图的割点与割边』

    无向图的割点与割边 定义:给定无相连通图\(G=(V,E)\) 若对于\(x \in V\),从图中删去节点\(x\)以及所有与\(x\)关联的边后,\(G\)分裂为两个或以上不连通的子图,则称\(x ...

  10. tarjan算法(强连通分量 + 强连通分量缩点 + 桥(割边) + 割点 + LCA)

    这篇文章是从网络上总结各方经验 以及 自己找的一些例题的算法模板,主要是用于自己的日后的模板总结以后防失忆常看看的, 写的也是自己能看懂即可. tarjan算法的功能很强大, 可以用来求解强连通分量, ...

随机推荐

  1. 基于TCP的通信程序设计

    套接字(Socket)是一种跨主机进程之间的双向通信接口,每个打开的套接字都可以通过一个套接字描述符来描述,因此可以使用低级文件编程库操作套接字. TCP是一中面向连接的网络传输控制协议.它每发送一个 ...

  2. openwrt 修改 banner

    http://www.network-science.de/ascii/ rectangles 风格

  3. 通过串口工具下发指令的Python脚本

    前言 最近一段时间在测试物联网相关的App自动化,涉及通过串口工具给硬件设备下发指令. 使用的串口工具:SecureCRT 解决办法 通过引用Python的第三方库:serial,通过编写Python ...

  4. Webkit JNI

    WebCoreFrameBridge.cpp BrowserFrame通过jni传下来的调用都会调用到WebCoreFrameBridge.cpp中的对应函数中,其他webkit的模块想回调信息给Br ...

  5. Struts2 实例(转载)

    一.准备工作及实例 1.解压struts-2.1.6-all.zip apps目录:struts2自带的例子程序docs目录:官方文档. lib 目录:存放所有jar文件. Src 目录:源文件存放地 ...

  6. c# SQLServer导入大批量数据

    说来惭愧,关于批量导入数据,一直采用的是最原始的方式,一条一条插入,或者100条一块批量插入,这种方式,五十步笑百步,并没有明显的性能提升, 昨天在从别的库查询数据到DataTable内存中,然后插入 ...

  7. yuicompressor

      yui/yuicompressor: YUI Compressor https://github.com/yui/yuicompressor    YUI Compressor 详细介绍 YUI ...

  8. it starts (“forks”) a new process for each connection.

    PostgreSQL: Documentation: 10: 1.2. Architectural Fundamentals https://www.postgresql.org/docs/10/st ...

  9. DuiLib笔记之Window常用属性

    caption 可拖拽以移动窗口的标题区,类型:RECT.例如,要指定标题区高度为35,可设置caption="0,0,0,35" mininfo 窗口最小尺寸,类型:SIZE.例 ...

  10. Linux环境下安装MySQL(解压方式)

    1.将安装包放在服务器上:mysql-5.6.37-linux-glibc2.12-x86_64.tar.gz 2.将安装包解压:tar -zxvf mysql-5.6.37-linux-glibc2 ...