Tarjan点的双联通(寻找割点)

本文详细介绍了点双连通图的概念与算法实现,包括割点与点双连通分量的定义、Tarjan算法原理及应用。通过具体实例展示了如何确定最少标记点数及其方案数量。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

问题概述:给你一个无向联通图,你要在图中标记一些点,使得这个图中的任意一个点消失了,剩余的点都可以通

一条路径到达你某个标记的点。问你最少需要选择多少个点,并且在最优的情况下有多少总选点方案,(每个样

例输入的第一个数m表示图中有多少条边,当m为0时读入结束)

输入样例:                                对应输出:

9                                                Case 1: 2 4

1 3   4 1

3 5   1 2

2 6   1 5

6 3   1 6   3 2

UVALive - 5135


点双连通图:该无向图中的任意两个顶点间都存在两条点不相交的路径

(或是说该图中去掉任意一个点都不会影响联通性)

定理:

①如果一个连通图不是点双联通图,那么必定存在至少一个点,如果这个点消失,图就会被分成多块,这样的点也

被称为割点

②点双联通分量一定是边双联通分量(除两点一线的特殊情况),反之不一定

③点双联通分量可以有公共点


Trajan主要原理:

time[k]:第一次遍历到k点的时间

low[k]:k点所在点双联通分量子图中第一个被搜到的点的time值

vis[k]:k点是否已经遍历

主要思路:一般来讲无论求解点双联通还是边双连通都是边入栈,如果点入栈,而因为性质③,你将一个点出

后,还可能有别的点双联通分量包含它,所以点入栈会导致一些错误和麻烦,但这里有种方法可以不需要栈,大概

思路是把所有的割点找出来(割点time[]值一定会小于等于他搜索子树儿子点的low[]值,至于为什么?很显然如果一个点不是割点,那么它必定在一环上或它自身是一个点双联通分量,考虑前者:那么这个点的深搜子树一定

与他的某个父亲相连接,这样这个儿子的low[]值就会是这个点某个父亲的low[]值,所以一定小于本身的time[]

值;考虑后者,它就不存在深搜子树更有儿子,根本不会比较),然后再搜索一遍找出所有在同一点双联通分量

中的点即可。

注意:搜索对于根节点因为没有父亲节点,所以无法通过上述方法判断根是否是割点,那怎么办?如果根有两个

搜索子树,那么它一定是个点,反之不是

搜索过程:

→当点k有与点c相连,如果此时(time[k]时)c没有搜过,搜索c点,当c点及其子树搜索完毕回溯后,low[k] =

 min(low[k], low[c]),如果此时low[c]的值>=time[k]的值,则说明点k是割点,否则不是

→当点k有与点c相连,如果此时(time[k]时)c已经搜过,low[k] = min(low[k], time[c])

期间保证:每个点每条边都只被搜索1次,且必须搜索1次,复杂度n+m,


此题题解:

看懂这题后第一眼想到是此图有多少个点双联通分量,那么就至少需要标记多少个点,然后情况数就是所有点双联

通分量中点个数的乘积,但没这么简单,要注意以下两点:

①:如果这个图本身是个点双联通分量,那么还是必须要标记两个点,因为有可能标记的点会被去掉,情况数就是

n*(n-1)/2,其中n为图中点的数量

②:如果一个点双联通分量中有不止一个割点,那么这个点双联通分量中就不需要标记任何一个点,因为其中一个

割点被去掉,还可以走另外一个割点到另一个双连通分量中


#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<vector>
#include<set>
using namespace std;
vector<int> G[50005];
set<int> st;
int n, t, sum, cutsum, vis[50005], low[50005], time[50005], cut[50005];
void Tarjan(int u, int p);
void Sech(int u);
int main(void)
{
	int i, m, u, v, cas = 1;
	long long bet, ans;
	while(scanf("%d", &m), m!=0)
	{
		cutsum = 0;
		t = n = 1;
		memset(vis, 0, sizeof(vis));
		memset(cut, 0, sizeof(cut));
		for(i=1;i<=m+2;i++)
			G[i].clear();
		for(i=1;i<=m;i++)
		{
			scanf("%d%d", &u, &v);
			G[u].push_back(v);
			G[v].push_back(u);
			n = max(n, max(v, u));
		}
		Tarjan(1, 0);
		memset(vis, 0, sizeof(vis));
		st.clear();
		ans = 1, bet = 0;
		for(i=1;i<=n;i++)
		{
			sum = 0;
			if(vis[i]==0 && cut[i]==0)
			{
				st.clear();
				Sech(i);
				if(st.size()==1)
				{
					ans *= sum;
					bet++;
				}
			}
		}
		if(cutsum==0)
			printf("Case %d: 2 %lld\n", cas++, (long long)n*(n-1)/2);
		else
			printf("Case %d: %lld %lld\n", cas++, bet, ans);
	}
	return 0;
}

void Tarjan(int u, int p)
{
	int i, v, child = 0;
	vis[u] = 1;
	low[u] = time[u] = t++;
	for(i=0;i<G[u].size();i++)
	{
		v = G[u][i];
		if(v==p)
			continue;
		if(vis[v]==0)
		{
			child++;
			Tarjan(v, u);
			low[u] = min(low[v], low[u]);
			if(p==0 && child>1 || p!=0 && low[v]>=time[u])
				cut[u] = 1,  cutsum++;
		}
		else
			low[u] = min(low[u], time[v]);
	}
}

void Sech(int u)
{
	int i, v;
	sum++;
	vis[u] = 1;
	for(i=0;i<G[u].size();i++)
	{
		v = G[u][i];
		if(cut[v])
			st.insert(v);			/*因为一个割点可能会被不同的点搜到,所以这里要用set防重*/
		if(cut[v] || vis[v])
			continue;
		Sech(v);
	}
}


### 关于Mooctest平台上的Tarjan算法题目与教程 #### Tarjan算法简介 Tarjan算法是一种用于解决图论问题的经典算法,主要应用于求解强连通分量(Strongly Connected Components, SCC)、(Articulation Points)以及桥(Bridges)。其核心思想基于深度优先搜索(DFS),通过维护时间戳和回溯边来高效解决问题。该算法的时间复杂度通常为 \(O(V + E)\),其中 \(V\) 表示顶数,\(E\) 表示边数[^1]。 #### Mooctest平台上关于Tarjan算法的内容 Mooctest作为一个在线编程学习和评测平台,提供了丰富的算法练习资源。对于Tarjan算法相关内容,可以通过以下方式查找: 1. **题目分类检索** 在Mooctest的题目列表中,可以按照标签筛选涉及“图论”的题目。具体到Tarjan算法的应用场景,如强连通分量、和桥等问题,可能会被标记为高级难度或特定主题下的挑战题[^2]。 2. **官方教程或指南** Mooctest可能提供针对Tarjan算法的基础讲解视频或文档,帮助初学者理解其实现细节及其应用场景。这类教程通常会附带模板代码,并结合实际案例分析如何优化性能[^3]。 以下是实现Tarjan算法的一个基本Python版本作为参考: ```python class TarjanSCC: def __init__(self, n): self.n = n self.graph = [[] for _ in range(n)] self.visited = [False] * n self.dfn = [-1] * n # 时间戳数组 self.low = [-1] * n # 回溯值数组 self.stack = [] self.sccs = [] # 存储所有的强连通分量 def add_edge(self, u, v): self.graph[u].append(v) def tarjan(self, u): index = len(self.stack) self.dfn[u] = self.low[u] = index self.stack.append(u) self.visited[u] = True for v in self.graph[u]: if self.dfn[v] == -1: # 如果v未访问过,则继续递归 self.tarjan(v) self.low[u] = min(self.low[u], self.low[v]) elif self.visited[v]: # 如果v已经在栈中,则更新low值 self.low[u] = min(self.low[u], self.dfn[v]) if self.dfn[u] == self.low[u]: # 找到了一个新的强连通分量 scc = [] while True: node = self.stack.pop() self.visited[node] = False scc.append(node) if node == u: break self.sccs.append(scc) # 使用示例 n = 5 edges = [(0, 1), (1, 2), (2, 0), (1, 3), (3, 4)] tarjan_scc = TarjanSCC(n) for edge in edges: tarjan_scc.add_edge(edge[0], edge[1]) tarjan_scc.tarjan(0) print(tarjan_scc.sccs) # 输出所有强连通分量 ``` 此代码实现了寻找有向图中的强连通分量功能,适用于基础训练需求。 #### 常见应用领域及相关扩展知识 - **强连通分量缩**:利用Kosaraju或者Tarjan算法先找出所有强连通分量,再将其压缩成单个节形成DAG(Directed Acyclic Graph),从而简化后续操作。 - **双联通分量分解**:进一步探讨无向图中的边双/双连通分量划分方法。 - **拓扑排序依赖关系处理**:当存在循环依赖时可通过检测是否存在环路来进行调整策略设计。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值