c语言tarjan算法,无向图求割点和割边——Tarjan算法

无向图中求割点集和割边集——Tarjan算法

割点和割边

定义

在一个无向图中,如果删除了某个顶点及与之相连的所有边,产生了一更大连通分量的子图,这样的顶点被称为割点或关节点。对于一个图的所有割点的集合被称为割点集。

同理,在无向图中,如果删除了某条边而产生了一个更大连通分量的子图,这样的边被称为割边或桥。对于一个图中所有割边的集合被称为割边集。

意义

几乎所有的问题都可以转化成图论相关的问题,而割点和割边作为无向图中不可或缺的属性,自然也有很重要的意义。比如在设计交通道路网的时候,设计者必须考虑问题就是如何尽量减少道路图中割点或割边的个数,因为如果存在割点或割边,此处的交通压力自然非常大,显然是设计的时候需要规避的。此外还有很多的应用。

如何求解割点和割边

对于图

math?formula=G(V%2CE),根据定义,如果要求解割点则需要三步:

BFS跑一遍图,记录下

math?formula=G(V%2CE)的连通分量为

math?formula=C

枚举所有顶点

math?formula=v_i并删除,再用BFS跑一边删除顶点后的子图,求出子图的连通分量

math?formula=C_i

比较

math?formula=C

math?formula=C_i,如果

math?formula=C_i%20%3C%20C则说明

math?formula=v_i是割点,反之不是。

由于要枚举所有顶点,而且对于每个结点都要遍历一遍子图,所以该算法的时间复杂度为

math?formula=T%20%3D%20%5Cbegin%7Bcases%7D%20O(%7CV%7C%5E3)%20%26%20%E9%82%BB%E6%8E%A5%E7%9F%A9%E9%98%B5%5C%5C%5B2ex%5D%20O((%7CV%7C%20%2B%20%7CE%7C)%5Ccdot%20%7CV%7C)%20%26%20%E9%82%BB%E6%8E%A5%E8%A1%A8%20%5Cend%7Bcases%7D

同理,根据定义求割边的算法时间复杂度为

math?formula=T%20%3D%20%5Cbegin%7Bcases%7D%20O(%7CE%7C%5Ccdot%7CV%7C%5E2)%20%26%20%E9%82%BB%E6%8E%A5%E7%9F%A9%E9%98%B5%20%5C%5C%5B2ex%5D%20O((%7CV%7C%2B%7CE%7C)%5Ccdot%7CE%7C)%20%26%20%E9%82%BB%E6%8E%A5%E8%A1%A8%20%5Cend%7Bcases%7D

Tarjan算法

算法提出者

割点和割边如此重要,如何利用计算机高效计算无向图的割点集和割边集也是计算机科学家们需要解决的问题。

Robert Tarjan,1986年图灵奖获得者提出了一种利用DFS回溯法解决无向图的割点割边问题的算法,故被称为Tarjan算法。顺便一提此,此人也对并查集的分析与应用做出了很大的贡献。

需要注意的是,Tarjan算法最初提出是为了解决强连通图中强连通分量问题的,这里也可以用它来解决无向图的割点和割边问题。

算法理论介绍

思考

在介绍理论之前我们先思考,除了定义之外,我们还能用什么方法判断割点和割边。可以这么想,如果一个顶点是割点,它或它的邻结点会有什么特殊的性质。

可以假

math?formula=v_i不是割点,其邻结点为

math?formula=v_%7Bi1%7D%2Cv_%7Bi2%7D%2C%5Cdots%2Cv_%7Bik%7D%20%5Ctext%7B%20%2Ck%7D

math?formula=v_i邻结点的总数,设

math?formula=Path(v_m%2Cv_k%2Cv_n)%20%3D%201表示

math?formula=v_m

math?formula=v_n之前存在至少一条不经过

math?formula=v_k的简单道路,

math?formula=Path(v_m%2Cv_k%2Cv_n)%3D0表示不存在这样的简单道路。则很容易得出

math?formula=%5Cforall%20m%20%5Cin%5B1%2Ck%5D%5Cforall%20n%20%5Cin%5B1%2Ck%5DPath(v_%7Bim%7D%2Cv_i%2Cv_%7Bin%7D)%3D1

则对这个问题的逆否命题为如果

math?formula=%5Ctext%7B%E5%AD%98%E5%9C%A8%20%7Dm%20%5Cin%5B1%2Ck%5D%5Ctext%7B%E5%AD%98%E5%9C%A8%20%7D%20n%5Cin%5B1%2Ck%5DPath(v_%7Bim%7D%2Cv_i%2Cv_%7Bin%7D)%3D0,那么

math?formula=v_i为割点且二者互为充要。同理,设

math?formula=PathEdge(v_m%2Ce_%7Bm-n%7D%2Cv_n)%20%3D%201表示

math?formula=v_m

math?formula=v_n是邻结点,且这两点之间存在至少一条不经过其直接相连的边

math?formula=e_%7Bm-n%7D而相连的简单道路,

math?formula=PathEdge(v_m%2Ce_%7Bm-n%7D%2Cv_n)%20%3D%200表示

math?formula=v_m%2Cv_n间不存在这样的一条道路。则类似可以推理出如果

math?formula=%5Ctext%7B%E5%AD%98%E5%9C%A8%20%7D%20m%20%5Cin%20%5B1%2Ck%5D%20PathEdge(v_i%2C%20e_%7Bi-im%7D%2Cv_%7Bim%7D)%3D0,那么

math?formula=e_%7Bi-im%7D为割边。

图的表示

在设计图论算法之前,我们需要考虑的就是如何在程序中表示一份图

math?formula=G(V%2CE)。结合算法的内容,我在这里用邻接表的方法存储一份无向图。

顶点的孩子,父亲和祖先

以任意顶点为根结点,采用DFS的策略从根结点遍历图。

一个顶点的孩子为在这种遍历方式下当前顶点的下一个顶点。

一个顶点的父亲为在这种遍历方式下当前顶点的上一个顶点,根结点没有父亲。

一个顶点的祖先为在这种遍历方式下当前顶点之前除了父亲的所有顶点,同理根结点没有祖先。

dfn数组和low数组

两个数组的空间大小为顶点个数

math?formula=%7CV%7C,分别表示每一个顶点的属性,假设所有顶点编号

math?formula=%5B0%2C%7CV%7C%20-%201%5D

math?formula=dfn%5Bi%5D表示在DFS回溯的前提下,该结点

math?formula=v_i被遍历的时间先后顺序,也就是时间戳,顺序递增。

math?formula=low%5Bi%5D表示在DFS回溯的前提下,该结点

math?formula=v_i不通过父亲到

math?formula=v_i这条边所能连通的顶点时间戳的最小值。初始

math?formula=low%5Bi%5D%3Ddfn%5Bi%5D

示例

844aa43b23ab

无向图

我们以

math?formula=v_0为根结点进行DFS回溯维护

math?formula=dfn%5Bi%5D

math?formula=low%5Bi%5D。时间戳从1开始。

844aa43b23ab

顶点v0

顶点如果有多个孩子,这里优先选择顶点编号较小的遍历。

844aa43b23ab

顶点v1

844aa43b23ab

顶点v3

844aa43b23ab

顶点v4

这里注意,顶点

math?formula=v_4下一个遍历的顶点是

math?formula=v_1,而

math?formula=dfn%5B1%5D%20%3C%20low%5B4%5D,所以

math?formula=low%5B4%5D%3Ddfn%5B1%5D%3D2

844aa43b23ab

重置顶点v4

844aa43b23ab

顶点v5

DFS处理到末尾后向前回溯,同时维护

math?formula=low%5Bfather%5D%3Dmin%5C%7Blow%5Bfather%5D%2Clow%5Bchildren%5D%5C%7D,这里回溯到顶点

math?formula=v_3,重置

math?formula=low%5B3%5D%3Dmin%5C%7Blow%5B3%5D%2Clow%5B4%5D%5C%7D%3D2

844aa43b23ab

重置顶点v3

844aa43b23ab

顶点v2

DFS回溯结束,

math?formula=dfn

math?formula=low都处理完毕。

割点与割边的判断

math?formula=dfn

math?formula=low都处理完毕之后,根据我们之前的分析,很容易得出,如果一个父亲

math?formula=v_f的所有孩子

math?formula=v_%7Bci%7D%EF%BC%8Ci%5Cin%5B0%2Ck%5D

math?formula=k

math?formula=v_f子结点总数。

如果

math?formula=%5Ctext%7B%E5%AD%98%E5%9C%A8%20%7D%20i%20%5Cin%5B0%2Ck%5D(low%5Bci%5D%20%3E%3D%20dfn%5Bv_f%5D),则

math?formula=v_f为割点。

如果

math?formula=%5Ctext%7B%E5%AD%98%E5%9C%A8%20%7Di%20%5Cin%5B0%2Ck%5D(low%5Bci%5D%20%3E%20dfn%5Bv_f%5D),则

math?formula=e_%7Bf-ci%7D为割边。

这也正说明了一个图中割边的数量永远不会超过割点的数量。

复杂度分析

math?formula=Tarjan算法仅用两次DFS回溯就可以得到所有割点和割边,不必暴力枚举,所以对于无向图

math?formula=G(V%2CE)在这种条件下时间复杂度为

math?formula=O(%7CV%7C%2B%7CE%7C)

程序展示(c++)

#include

#include

#include

#include

using namespace std;

const int maxn = 1e3 + 10;

const int maxm = 1e6 + 10;

int tst; // timestamp

int N, M; // 顶点数N,边数M

struct Edge {

int from, to;

Edge(int f = 0, int t = 0):

from(f), to(t) { }

~Edge() { }

};

// adj-list undirected graph

// idx -> even with compone idx + 1.

vector edges; //所有边的集合

vector ver[maxn]; //记录每一个顶点出发的边在edges中的索引

int dfn[maxn];

int low[maxn];

vector verCut; //割点集

vector edgeCut; //割边集

/**

* @brief 读取数据,以邻接表方式储存,因为是无向图需要一条边存两次

* 第1行输入N和M表示图的顶点数和边数

* 接下来第2到第M + 1行输入from和to表示无向图的一条边的两个顶点.

*/

void readData() {

cin >> N >> M;

for (int inc = 0; inc < M; inc ++) {

int from = 0, to = 0;

cin >> from >> to;

ver[from].push_back(edges.size());

edges.push_back(Edge(from, to));

ver[to].push_back(edges.size());

edges.push_back(Edge(to, from));

}

}

/**

* @brief DFS回溯上标签

* @param curr_ver 当前顶点的编号

* @param fa_ver 当前顶点的父亲编号,根节点父亲设为-1

*/

void labelling(int curr_ver = 0, int fa_ver = -1) {

dfn[curr_ver] = low[curr_ver] = ++tst; //初始化dfn和low

for (vector::iterator iter = ver[curr_ver].begin();

iter != ver[curr_ver].end(); iter ++) {

if (edges[*iter].to == fa_ver)

continue;

if (!low[edges[*iter].to]) {

labelling(edges[*iter].to, curr_ver);

}

low[curr_ver] = min(low[curr_ver], low[edges[*iter].to]);

}

}

/**

* @brief 收集统计割点及割边

*/

void collect(int curr_ver = 0, int fa_ver = -1) {

tst++;

bool is_vercut = false;

for (vector::iterator iter = ver[curr_ver].begin();

iter != ver[curr_ver].end(); iter++) {

if (edges[*iter].to == fa_ver || dfn[edges[*iter].to] != tst + 1)

continue;

collect(edges[*iter].to, curr_ver);

int sub = low[edges[*iter].to] - dfn[curr_ver];

is_vercut = (sub >= 0);

if (sub > 0) {

edgeCut.push_back(Edge(curr_ver, edges[*iter].to));

}

}

if (is_vercut) {

verCut.push_back(curr_ver);

}

}

/**

* @brief tarjan算法,包含两个子函数,调用函数前需要将时间戳重置

*/

void tarjan() {

tst = 0;

labelling();

tst = 0;

collect();

}

void show() {

cout << "vertex cut(s):" << endl;

cout << "total: " << verCut.size() << endl;

for (vector::iterator iter = verCut.begin();

iter != verCut.end(); iter ++) {

cout << (*iter) << endl;

}

cout << "edge cut(s):" << endl;

cout << "total: " << edgeCut.size() << endl;

for (vector::iterator iter = edgeCut.begin();

iter != edgeCut.end(); iter ++) {

cout << (*iter).from << " to " << (*iter).to << endl;

}

}

int main() {

//freopen("Tarjan.txt", "r", stdin);

readData();

tarjan();

show();

return 0;

}

/*****测试数据******

6 6

0 1

1 3

3 4

4 5

1 4

1 2

*******************/

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值