关于hihocoder上连通分量的学习

详细资料可以参看:

http://hihocoder.com/problemset/problem/1183

http://hihocoder.com/problemset/problem/1184

http://hihocoder.com/problemset/problem/1185

http://hihocoder.com/problemset/problem/1186


这里仅仅是重新梳理一下系列教程中的一些概念而已。


首先是无向图。


先记住这几个概念,记住了就比较容易理解连通性问题。

割边:在连通图中删掉一条边后图就不再连通,这样的边叫割边,也叫桥。

割点:在连通图中删掉一个点和其所有连接的边图就不再连通,这样的点叫割点。

DFS搜索树:用DFS对图进行遍历时,按照遍历次序的不同,可以得到一棵DFS搜索树。

树边:在DFS过程中访问未访问节点所经过的边,也称为父子边。

回边:在DFS过程中遇到已访问节点时所经过的边,也称为返祖边,后向边。


可以得到结论:

1. 割边的端点一定是割点;

2. 割点连接的边不一定是割边,甚至可能一条割边都没有。


查找割点,判断两种情况:

1.对DFS树的根,如果有两棵或以上的子树,则根节点是割点。直观的理解,子树之间的点自然是不连通的,如果根删掉后,子树与子树就被分割了。

2.对于非叶子节点(同时也不是根节点),若其中的某棵子树的节点均没有指向其祖先节点,说明删除该节点后,根与该棵子树的节点不再连通,该节点为割点。


对于叶子节点呢?叶子节点不可能是割点,因为它的边或者是回边,或者是连接一个父节点,而父节点的连通性在它被删除后不受影响。


对于非叶子节点判断的逻辑如下,用dfn[u]记录节点u在DFS过程中被遍历到的次序号,low[u]记录节点u或u的子树通过非父子边追溯到的最早的祖先节点(即DFS序号最小),那么low[u]的计算过程如下:

low[u] = 1. min(low[u], low[v]) | (u, v)为树边

      2. min(low[u], dfn[v]) | (u, v)为回边且v不为u的父亲节点

为什么要求v不为u的父亲节点?是为了保证low[u]永远被其下游的节点更新。


Tarjan算法:

// Tarjan
void dfs(int u) {
	static int counter = 0; 
	int children = 0; 

	ArcNode *p = graph[u].firstArc; 
	visit[u] = 1; 

	dfs[u] = low[u] = ++counter; 

	for (; p != NULL; p = p->next) {
		int v = p->adjvex; 
		if (!visited[v]) {
			children++;
			parent[v] = u; 
			dfs(v);

			low[u] = min(low[u], low[v]);

			if (parent[u] == NIL && children > 1) {
				printf("articulation point: %d", u);
			}

			if (parent[u] != NIL && low[v] >= dfn[u]) {
				printf("articulation point: %d\n", u);
			}

			if (low[v] > dfn[u]) {
				printf("bridge: %d %d\n", u, v);
			}
		}
		else if (v != parent[u]) {
			low[u] = min(low[u], dfn[v]);
		}
	}
}



边的双连通分量:

对于一个无向图的子图,当删除其中任意一条边后,不改变图内点的连通性,这样的子图叫做边的双连通子图。而当子图的边数达到最大时,叫做边的双连通分量。

直观的方法,删除所有的桥,然后求出所有的连通分量。

代码:

// Tarjan
void dfs(int u) {
	static int counter = 0; 

	int children = 0; 
	ArcNode *p = graph[u].firstArc; 
	visit[u] = 1; 

	dfn[u] = low[u] = ++counter; 

	stack[++top] = u; 

	for (; p != NULL; p = p->next) {
		int v = p->adjvex;
		if (!visit[v]) {
			children++;
			parent[v] = u; 
			dfs(v);

			low[u] = min(low[u], low[v]);
			if (low[v] > dfn[u]) {
				printf("bridge: %d %d\n", u, v);
				bridgeCnt++;
			}
		}
		else if (v != parent[u]) {
			low[u] = min(low[u], dfn[v]);
		}
	}

	if (low[u] == dfn[u]) {
		// 因为low[u] == dfn[u],对(parent[u], u)来说有dfn[u] > dfn[ parent[u] ],
		// 因此low[u] > dfn[ parent[u] ]
		// 所以(parent[u],u)一定是一个桥,那么此时栈内在u之前入栈的点和u被该桥分割开
		// 则u和之后入栈的节点属于同一个组
		// 将从u到栈顶所有的元素标记为一个组,并弹出这些元素。
	}
}

在这里要注意最后判断属于同一个连通分量的逻辑在对所有连接边遍历之后作。


强连通分量:

对于有向图上的2个点a,b,若存在一条从a到b的路径,也存在一条从b到a的路径,那么称a,b是强连通的。

对于有向图上的一个子图,若子图内任意点对(a,b)都满足强连通,则称该子图为强连通子图。

非强连通图有向图的极大强连通子图,称为强连通分量。

特别的,和任何一个点都不强连通的单个点也是一个强连通分量。

代码:

// Tarjan
// 求强连通分量
void dfs(int u) {
	dfn[u] = low[u] = ++Index; 
	stack.push(u);
	for each (u, v) in E{
		if (v is not visited) {
			tarjan(v);
			low[u] = min(low[u], low[v]);
		}
		else if (v in stack) {
			low[u] = min(low[u], dfn[v]);
		}
	}

	if (dfn[u] == low[u]) {
		do {
			v = stack.top();
			mark(v);
		} while (u != v); 
	}
}

代码和上面的寻找双连通分量很像。

点的双连通分量。

定义:对于一个无向图的子图,当删除其中任意一个点后,不改变图内点的连通性,这样的子图叫做点的双连通子图。而当子图的边数达到最大时,叫做点的双连通分量。

有一点很特殊,一个点是可以属于多个连通分量的。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值