【模板】 tarjan 求强连通分量、割点、割边、缩点

本文深入讲解图算法中的关键概念,包括强连通分量、割点、割边、缩点等,通过具体代码示例帮助理解算法实现。涵盖求解强连通块、识别割点与割边、缩点技巧及其实现细节。

强连通 割点 割边 视频:https://www.bilibili.com/video/av41752079/?p=1 (细节讲的很清楚了)

求强连通、缩点而言是有向图

求割点、割边而言是无向图

解释看代码


求强连通块:

习题:https://www.luogu.org/problemnew/show/P2921

代码:

#include <iostream>
#include <cstring>
#include <iomanip>
#include <algorithm>
#include <queue>
#include <stack>
#include <vector>
#include <set>
#include <map>
#include <cmath>
#include <cstdio>
using namespace std;
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
const ll maxn = 1e5+100;
const ll mod = 998244353;
const ld pi = acos(-1.0);
const ll inf = 1e18;

ll n,dfn[maxn],low[maxn]; //inf表示第几个访问该点,low表示当前点能返回的最早时间戳
vector<ll>G[maxn];  //存图 
stack<ll>s;   
bool instack[maxn];  //标记是否在栈中 
ll timing;  //时间戳 
ll colorcnt;   //强连通分量数量 
ll color[maxn];  //第i个点属于哪个强连通分量 或者染成了哪种颜色 
ll colornum[maxn]; //该强连通分量里有多少个点 
ll ans[maxn];

void tarjan(ll now)
{	
	timing++;
	dfn[now] = low[now] = timing;
	s.push(now);
	instack[now] = true;
	
	for(ll i = 0; i < G[now].size(); i++)
	{
		ll to = G[now][i];
		
		//cout << to << endl;
		
		if(dfn[to] == 0)   //如果未访问
		{	
			tarjan(to);
			low[now] = min(low[now],low[to]);
		}
		else if(instack[to] == true)   //如果访问过且在栈中(属于同一强连通分量)
		    low[now] = min(low[now],dfn[to]);
			
			
	} 
	
	if(low[now] == dfn[now])  //找到该连通分量的初始点
	{
		colorcnt++;   
		while( s.top() != now )
		{
			ll tmp = s.top();
			s.pop();
			
			instack[tmp] = false;  //不要忘了清楚标记
			color[tmp] = colorcnt;
			colornum[colorcnt]++; 
		}
		
		ll tmp = s.top();  //处理初始点
		s.pop();
		
		color[tmp] = colorcnt;
		colornum[ colorcnt ]++;
		instack[tmp] = false;
	}
	
	return ;	
}

ll slove(ll now)
{
	if(now == G[now][0])
		return 1;
	
	if(ans[ G[now][0] ] != 0)
		return 1+ans[ G[now][0] ];
	else
		return 1+slove( G[now][0]  );
}

int main()
{
    ios::sync_with_stdio(false);
    //cin.tie(0),cout.tie(0);
	
	cin >> n;
		
	for(ll i = 1; i <= n; i++)
	{
		ll y;
		cin >> y;
		G[i].push_back(y);
	}
	
	
	for(ll i = 1; i <= n; i++)
	{
		if(dfn[i] == 0)
			tarjan(i);
	}
	
	/*for(ll i = 1; i <= n; i++)
	{
		cout << dfn[i] << " " << low[i] << endl;
	}
	
	cout << endl;*/
	
	for(ll i = 1; i <= n; i++)
	{
		if(colornum[ color[i] ] > 1)
			ans[i] = colornum[ color[i] ];
	}
	
	for(ll i = 1; i <= n; i++)
	{
		if(ans[i] == 0)
		{
			ans[i] = slove(i);
		}
	}
	
	for(ll i = 1; i <= n; i++)
	{
		cout << ans[i] << endl;
	}

	return 0;
}
/*
6 8
1 2
2 3
3 6
1 4
4 5
5 1
5 6
2 5
*/

求割点:

对于根节点的判断标准是有两颗不连通的子树就是割点

对于普通节点的判断标准是 low[to] >= dfn[now]  (也不能是根节点)

习题:https://www.luogu.org/problemnew/show/P3388

#include <iostream>
#include <cstring>
#include <iomanip>
#include <algorithm>
#include <queue>
#include <stack>
#include <vector>
#include <set>
#include <map>
#include <cmath>
#include <cstdio>
using namespace std;
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
const ll maxn = 2e4+100;
const ll mod = 998244353;
const ld pi = acos(-1.0);
const ll inf = 1e18;

ll n,m,dfn[maxn],low[maxn],color[maxn],colornum[maxn],timing,colorcnt;
vector<ll>G[maxn];
set<ll>ans;

void tarjan(ll now,ll rt)   //rt是传进来的根节点,,固定不变,传下去
{
    ll child = 0;  //判断根节点是否有左右子树
    timing++;
    dfn[now] = low[now] = timing;
    
    for(ll i = 0; i < G[now].size(); i++)
    {
        ll to = G[now][i];
        if(dfn[to] == 0)
        {
            child++;
            tarjan(to,rt);
            low[now] = min(low[now],low[to]);
            
            if(low[to] >= dfn[now] && now != rt)  //对于普通节点的判断
            {
                ans.insert(now);
            }
            
        }
        
        low[now] = min(low[now],dfn[to]);  //更新自身
        
            
    }
    
    if(now == rt && child >= 2)  //对于根节点判断
    {
        ans.insert(now);
    }
    
    return ;
}

int main()
{
    ios::sync_with_stdio(false);
    //cin.tie(0),cout.tie(0);
    
    cin >> n >> m;
    
    for(ll i = 1; i <= m; i++)
    {
        ll x,y;
        cin >> x >> y;
        G[x].push_back(y);
        G[y].push_back(x);
    }
    
    for(ll i = 1; i <= n; i++)
    {
        if(dfn[i] == 0)
            tarjan(i,i);
    }
    
    //cout << colorcnt << endl;
    
    cout << ans.size() << endl;
    
    set<ll>::iterator it = ans.begin(); 
    
    while(it != ans.end())
    {
        cout << *it << " ";
        it++;
    } 
    cout << endl;
    
    return 0;
}

求割边:搜索时带着前驱节点,在自己不是自己父亲时更新

 low[to] > dfn[now]  是没有等于号的,视频里有讲解为什么不能有等于号 

习题:https://ac.nowcoder.com/acm/contest/392/I

#include <iostream>
#include <cstring>
#include <iomanip>
#include <algorithm>
#include <queue>
#include <stack>
#include <vector>
#include <set>
#include <map>
#include <cmath>
#include <cstdio>
using namespace std;
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
const ll maxn = 1e5+100;
const ll mod = 998244353;
const ld pi = acos(-1.0);
const ll inf = 1e18;

ll n,m,ans,timing;
vector<ll>G[maxn];
ll dfn[maxn],low[maxn];

void tarjan(ll now,ll pre)
{
	timing++;
	low[now] = dfn[now] = timing;
	
	for(ll i = 0; i < G[now].size(); i++)
	{
		ll to = G[now][i];
		
		if(dfn[to] == 0)
		{
			tarjan(to,now);
			low[now] = min(low[now],low[to]);
			
			if(low[to] > dfn[now])
			{
				ans++;  //to到now 是一条割边 
			}
			
		}
		else if(to != pre)  //自己不是自己父亲时更新
		{
			low[now] = min(low[now],dfn[to]);
		}
		
	}
	
}

int main()
{
    ios::sync_with_stdio(false);
    //cin.tie(0),cout.tie(0);
	
	cin >> n >> m;
	
	for(ll i = 1; i <= m; i++)
	{
		ll x,y;
		cin >> x >> y;
		G[x].push_back(y);
		G[y].push_back(x);
	}	
	
	for(ll i = 1; i <= n; i++)
	{
		if(dfn[i] == 0)
		{
			tarjan(i,i);
		}
	}
	
	cout << m - ans << endl;
	
	return 0;
}




缩点:缩点与求强连通块类似

习题:http://poj.org/problem?id=2186

#include <iostream>
#include <cstring>
#include <iomanip>
#include <algorithm>
#include <queue>
#include <stack>
#include <vector>
#include <set>
#include <map>
#include <cmath>
#include <cstdio>
using namespace std;
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
const ll maxn = 1e4+100;
const ll mod = 998244353;
const ld pi = acos(-1.0);
const ll inf = 1e18;

ll n,m,timing,ans;
vector<ll>G[maxn];
ll dfn[maxn],low[maxn];
ll color[maxn];
ll colornum[maxn];
ll colorcnt;
ll du[maxn];  //表示第i个强连通分量有几个出边 
stack<ll>s;
bool instack[maxn];

void tarjan(ll now)
{
	timing++;
	s.push(now);
	instack[now] = true;
	low[now] = dfn[now] = timing;
	
	for(ll i = 0; i < G[now].size(); i++)
	{
		ll to = G[now][i];
		
		if(dfn[to] == 0)
		{
			tarjan(to);
			low[now] = min(low[now],low[to]);
		}
		else if(instack[to] == true)
		{
			low[now] = min(low[now],dfn[to]);
		}
		
	}
	
	if(dfn[now] == low[now])
	{
		
		ll tmp;
		colorcnt++;
		while(s.top() != now)
		{
			tmp = s.top();
			s.pop();
			
			color[tmp] = colorcnt;
			colornum[colorcnt]++;
			instack[tmp] = false;
		}
		
		tmp = s.top();
		s.pop();
		
		color[tmp] = colorcnt;
		colornum[colorcnt]++;
		instack[tmp] = false;
			
	}
	
	return ;
}

int main()
{
    ios::sync_with_stdio(false);
    //cin.tie(0),cout.tie(0);
	
	cin >> n >> m;
	
	for(ll i = 1; i <= m; i++)
	{
		ll x,y;
		cin >> x >> y;
		G[x].push_back(y);
	}	
	
	for(ll i = 1; i <= n; i++)
	{
		if(dfn[i] == 0)
		{
			tarjan(i);
		}
	}
	
	//cout << colorcnt << endl;
	
	for(ll i = 1; i <= n; i++)     //进行缩点  
	{
		for(ll j = 0; j < G[i].size(); j++)  
		{
			ll to = G[i][j];
			if(color[i] != color[to])
			{
				du[ color[i] ]++;
			}
		}
		
	}
	
	ll num = 0,ans = 0; //num出度为0的点 
	for(ll i = 1; i <= colorcnt; i++)
	{
		if(du[i] == 0)
		{
			num++;
			ans = colornum[i];
		}	
	}
	
	if(num >= 2 || num == 0)
	{
		cout << 0 << endl;
	}
	else if(num == 1)
	{
		cout << ans << endl;
	}
	
	
	
	return 0;
}



 

<think>好的,用户想了解双连通分量的概念、法,以及洛谷P8435的C++代码。首先,我需要回顾双连通分量的定义。双连通分量是无向图中的极大子图,满足删除任意一个顶后子图仍然连通。这意味着图中没有,或者说任意两间存在至少两条不重复的路径。 接下来,关于法,通常使用Tarjan算法。这里需要注意,双连通分量的Tarjan算法和双连通分量或强连通分量有所不同。需要记录dfn和low数组,同时利用栈来保存当前路径上的。当发现某个顶u的子节v无法通过回到达u的祖先时,u就是,此时需要将栈中的弹出,直到当前,这些属于同一个双连通分量。 然后,用户需要C++实现的洛谷P8435模板题代码。我需要参考已有的Tarjan算法实现,特别是处理双的部分。根据引用中的代码,比如引用3中的强连通分量模板,但双的处理方式不同,需要使用顶栈,并且在发现时提取分量。需要注意的是,每个可能属于多个双,而非则只属于一个双。 代码结构方面,需要构建无向图,使用dfn和low数组,栈来保存节,当检测到low[v] >= dfn[u]时,说明u是,开始弹出节直到v。同时,根节需要特殊处理,如果根的子节数大于1,则根也是。 在编写代码时,要注意无向图的处理,避免重复访问。通常每条存储两次,所以遍历时需记录父节来防止回溯。此外,每个双连通分量至少包含两个顶或一个(如两个顶一条的情况)。 最后,根据洛谷P8435的要,输出每个双连通分量的成员。需要测试代码的正确性,确保所有情况覆盖,比如孤立、自环等特殊情况可能不需要考虑,但按题目输入处理即可。 总结步骤:解释双概念,Tarjan算法的步骤,给出C++代码,并确保代码正确处理和分量的提取,同时符合题目要的输出格式。</think>### 双连通分量概念 **双连通分量**(Biconnected Component)是无向图中的极大子图,满足删除任意一个顶后子图仍然保持连通。即:任意两间存在至少两条不重复的路径[^1]。例如,环结构是典型的双连通分量,而连接两个环的公共顶(即)会将图分为多个双连通分量。 ### 法:Tarjan算法 1. **判定**:顶$u$是当且仅当满足: - 若$u$是根节,且子树数量$≥2$ - 若$u$非根,且存在子节$v$满足$low[v] \geq dfn[u]$ 2. **算法流程**: - 维护栈记录遍历路径 - 发现时,从栈中弹出节构成一个双连通分量 - 属于多个双,需保留在栈中 --- ### C++实现(洛谷P8435模板题) ```cpp #include <bits/stdc++.h> using namespace std; const int N = 5e5 + 10; vector<int> g[N], ans[N]; int dfn[N], low[N], stk[N], idx, top, cnt; int n, m; void tarjan(int u, int fa) { dfn[u] = low[u] = ++idx; stk[++top] = u; int child = 0; for (int v : g[u]) { if (!dfn[v]) { child++; tarjan(v, u); low[u] = min(low[u], low[v]); if (low[v] >= dfn[u]) { // 判定 cnt++; int x; do { x = stk[top--]; ans[cnt].push_back(x); } while (x != v); // 弹出至v(不弹出u) ans[cnt].push_back(u); // 将u加入当前双 } } else if (v != fa) { low[u] = min(low[u], dfn[v]); } } if (fa == 0 && child == 0) // 孤立单独成块 ans[++cnt].push_back(u); } int main() { cin >> n >> m; for (int i = 0; i < m; i++) { int u, v; scanf("%d%d", &u, &v); if (u == v) continue; // 自环无需处理 g[u].push_back(v); g[v].push_back(u); } for (int i = 1; i <= n; i++) if (!dfn[i]) { top = 0; tarjan(i, 0); } cout << cnt << endl; for (int i = 1; i <= cnt; i++) { printf("%d ", ans[i].size()); for (int x : ans[i]) printf("%d ", x); puts(""); } return 0; } ``` #### 代码说明 1. **输入处理**:忽略自环 2. **Tarjan核心**: - 使用栈记录遍历路径 - 当发现时,弹出子节部分构成双,保留 3. **孤立处理**:单独作为双输出 4. **复杂度**:$O(n + m)$ ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值