Tarjan算法模板

一、最近公共祖先(LCA)

LCALeast Common Ancestor

P3379 【模板】最近公共祖先(LCA)

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;

ll quickin(void)
{
	ll ret = 0;
	bool flag = false;
	char ch = getchar();
	while (ch < '0' || ch > '9')
	{
		if (ch == '-')    flag = true;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9' && ch != EOF)
	{
		ret = ret * 10 + ch - '0';
		ch = getchar();
	}
	if (flag)    ret = -ret;
	return ret;
}

// 存查询的数据结构 
typedef pair<int, int> P;  // first -- 另一个节点; second -- 查询编号 

const int MAX = 5e5 + 3;
int N, M, S;               // N -- 节点数; M -- 查询数; S -- 根节点 
vector<int> G[MAX];        // 存树,按无向图存储 
vector<P> query[MAX];      // 存查询,双向存 
int par[MAX], ans[MAX];    // par[] -- 各节点的父节点; ans[] -- 查询答案,按编号存储 
bool vis[MAX];             // vis[] -- 是否访问该节点 

// 并查集初始化 
void init(void)
{
	for (int i = 1; i <= N; ++i)
		par[i] = i;
}

// 并查集查询 
int find(int x)
{
	if (par[x] == x)    return x;
	else
		return par[x] = find(par[x]);
}

// tarjan算法 + 并查集 
void tarjan(int u)
{
	vis[u] = true;                         // 入u, 标记u
	
	// 遍历u的每条边 
	for (int i = 0; i < G[u].size(); ++i)
	{
		int v = G[u][i];
		if (!vis[v])                       // 防止访问父节点 
		{
			tarjan(v);
			par[v] = u;                    // 回u, v指向u 
		}
	}
	
	// 离u, 处理查询 
	for (int i = 0; i < query[u].size(); ++i)
	{
		P p = query[u][i];
		int v = p.first, id = p.second;
		if (vis[v])    ans[id] = find(v);      // 若v被访问过,则v的根节点即所求 
	}
}

int main()
{
	#ifdef LOCAL
	freopen("test.in", "r", stdin);
	#endif
	
	N = quickin(), M = quickin(), S = quickin();
	for (int i = 0; i < N - 1; ++i)
	{
		int a, b;
		a = quickin(), b = quickin();
		G[a].push_back(b);               // 双向存边 
		G[b].push_back(a);
	}
	for (int i = 0; i < M; ++i)
	{
		int a, b;
		a = quickin(), b = quickin();
		query[a].push_back(P(b, i));     // 双向存查询; i -- 查询编号 
		query[b].push_back(P(a, i));
	}
	
	init();
	tarjan(S);
	for (int i = 0; i < M; ++i)
		cout << ans[i] << endl;
	
	return 0;
}

二、强连通分量(SCC)

SCCStrongly Connected Component

B3609 [图论与代数结构 701] 强连通分量

1、基本概念

搜索树:对图深搜时,每个节点仅访问一次,节点按被访问的顺序和访问时经过的有向边组成搜索树
有向边的分类:

  • 树边:搜索树中的边
  • 返祖边:指向祖先节点的边
  • 横插边:右子树指向左子树的边
  • 前向边:指向子孙节点的边

定理1:返祖边与树边必定构成环,横插边可能与树边构成环,前向边无用。
定理2:强连通分量以树的形式存在于搜索树中,每个强连通分量都有一个根,其余节点都在根的子树中。

2、Code

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;

ll quickin(void)
{
	ll ret = 0;
	bool flag = false;
	char ch = getchar();
	while (ch < '0' || ch > '9')
	{
		if (ch == '-')    flag = true;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9' && ch != EOF)
	{
		ret = ret * 10 + ch - '0';
		ch = getchar();
	}
	if (flag)    ret = -ret;
	return ret;
}

const int MAX = 1e4 + 3;
int N, M;                       // N -- 节点数; M -- 边数 
vector<int> G[MAX];             // 存有向图 
int dfn[MAX], low[MAX], tot;    
/*
dfn[] -- 时间戳,各节点第一次被访问的顺序(也可以起到vis[]的作用) 
low[] -- 从该节点出发,能访问到的最早的时间戳
tot -- 更新时间戳 
*/
stack<int> S;                   // 将节点按时间戳压栈 
bool instk[MAX];                // 该节点是否在栈中 
vector<vector<int>> ans;        // 存储强连通分量 

void tarjan(int u)
{
	// 入u,盖戳,入栈 
	dfn[u] = low[u] = ++tot;
	S.push(u), instk[u] = true;
	
	// 遍历u的每条边 
	for (int i = 0; i < G[u].size(); ++i)
	{
		int v = G[u][i];
		if (!dfn[v])                       // 未访问过v(在搜索树中,v是u的孩子) 
		{
			tarjan(v);
			low[u] = min(low[u], low[v]);  // 回u,更新low 
		}
		else if (instk[v])                 // 访问过v,且在栈中(在搜索树中,v是u的祖先) 
		{
			low[u] = min(low[u], dfn[v]);
		}
	}
	
	// 离u,处理强连通分量 
	if (dfn[u] == low[u])
	{
		vector<int> scc;
		int t;
		do
		{
			t = S.top();
			S.pop(), instk[t] = false;
			scc.push_back(t);
		} while (t != u);
		
		sort(scc.begin(), scc.end());
		ans.push_back(scc);
	}
}

bool cmp(const vector<int> &a, const vector<int> &b)
{
	return a[0] < b[0];
}

int main()
{
	#ifdef LOCAL
	freopen("test.in", "r", stdin);
	#endif
	
	N = quickin(), M = quickin();
	for (int i = 0; i < M; ++i)
	{
		int a, b;
		a = quickin(), b = quickin();
		G[a].push_back(b);                  // 存储有向边 
	}
	
	// 图未必是连通的 
	for (int i = 1; i <= N; ++i)
	{
		if (!dfn[i])
			tarjan(i);
	}
	
	sort(ans.begin(), ans.end(), cmp);
	cout << ans.size() << endl;
	for (int i = 0; i < ans.size(); ++i)
	{
		for (int j = 0; j < ans[i].size(); ++j)
		{
			cout << ans[i][j] << ' ';
		}
		cout << endl;
	}
	
	return 0;
}

割点

P3388 【模板】割点(割顶)

  • 割点:对于一个无向图,如果把一个点删除后,连通块的个数增加了,那么这个点就是割点(割顶)。
  • 割点的判断:
    \(u\) 不是根节点,在搜索树中,存在一个子节点,满足 \(low[v]>=dfn[u]\),则 \(u\) 就是割点。
    \(u\) 是根节点,在搜索树中,存在两个及以上子节点,满足 \(low[v]>=dfn[u]\),则 \(u\) 就是割点。
#include <bits/stdc++.h>

using namespace std;
typedef long long ll;

ll quickin(void)
{
	ll ret = 0;
	bool flag = false;
	char ch = getchar();
	while (ch < '0' || ch > '9')
	{
		if (ch == '-')    flag = true;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9' && ch != EOF)
	{
		ret = ret * 10 + ch - '0';
		ch = getchar();
	}
	if (flag)    ret = -ret;
	return ret;
}

const int MAX = 2e4 + 3;
int N, M;
vector<int> G[MAX];
int dfn[MAX], low[MAX], tot;
bool cut[MAX];           // cnt[] -- 是否是割点
int root, cnt;           // root -- 根节点; cnt -- 割点个数 

void tarjan(int u)
{
	// 入u,盖戳 
	dfn[u] = low[u] = ++tot;
	int child = 0;             // 记录满足判断条件的孩子个数 
	
	for (int v : G[u])
	{
		if (!dfn[v])   // v未访问过 
		{
			tarjan(v);
			// 回u,更新low 
			low[u] = min(low[u], low[v]);
			
			if (low[v] >= dfn[u])             // 割点的前提条件 
			{
				++child;
				if (u != root || child > 1)   // 非根节点,有一个孩子即可;根节点,需要一个以上 
					if (!cut[u])
					{
						cut[u] = true;
						++cnt;                // 记录割点个数 
					}
			}
		}
		else    // v已经访问过 
			low[u] = min(low[u], dfn[v]);
	}
}

int main()
{
	#ifdef LOCAL
	freopen("test.in", "r", stdin);
	#endif
	
	N = quickin(), M = quickin();
	for (int i = 0; i < M; ++i)
	{
		int a, b;
		a = quickin(), b = quickin();
		G[a].push_back(b);               // 无向图 
		G[b].push_back(a);
	}
	
	// 图可能不连通 
	for (int i = 1; i <= N; ++i)
		if (!dfn[i])
		{
			root = i;              // 第一次访问的节点是搜索树的根节点 
			tarjan(i);
		}
	
	cout << cnt << endl;
	for (int i = 1; i <= N; ++i)
		if (cut[i])    cout << i << ' ';
	
	return 0;
}

割边

P1656 炸铁路

  • 割边:对于一个无向图,如果删掉一条边后,图中的连通块个数增加了,则该边就是割边(桥)。
  • 判断割边的法则:
    在搜索书中,节点 \(u\) 存在一个子节点 \(v\),满足 \(low[v] > dfn[u]\),则 \((u,v)\) 是割边。

两种实现方式:
链式邻接表

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;

ll quickin(void)
{
	ll ret = 0;
	bool flag = false;
	char ch = getchar();
	while (ch < '0' || ch > '9')
	{
		if (ch == '-')    flag = true;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9' && ch != EOF)
	{
		ret = ret * 10 + ch - '0';
		ch = getchar();
	}
	if (flag)    ret = -ret;
	return ret;
}

struct edge{int from, to;};
struct bridge{int x, y;};

const int MAX = 153;
int N, M;
// 采用链式邻接表存储 
vector<edge> E;                 // 存边 
vector<int> H[MAX];             // 存u为起点的边的编号(编号从0开始) 
int dfn[MAX], low[MAX], tot;
vector<bridge> ans;             // 存桥 

void add(int u, int v)
{
	E.push_back(edge{u, v});          // 存uv边 
	H[u].push_back(E.size() - 1);     // 在u的表头中存uv边的编号 
}

void tarjan(int u, int in_edge)   // in_edge -- 进入u的边的编号 
{
	// 入u,盖戳 
	dfn[u] = low[u] = ++tot;
	// 遍历u为起点的边 
	for (int i : H[u])
	{
		int v = E[i].to;
		if (!dfn[v])                          // 未访问v 
		{
			tarjan(v, i);
			low[u] = min(low[u], low[v]);     // 回u,更新low 
			
			if (low[v] > dfn[u])              // 桥的条件 
				ans.push_back(bridge{u, v});
		}
		else if (i != (in_edge ^ 1))          // 已访问v,且不是反边 
			low[u] = min(low[u], dfn[v]);     // 正反边01、23、45等编号,同过^ 1相互转换 
	}
}

bool cmp(const bridge &a, const bridge &b)
{
	if (a.x == b.x)    return a.y < b.y;
	return a.x < b.x; 
}

int main()
{
	#ifdef LOCAL
	freopen("test.in", "r", stdin);
	#endif
	
	N = quickin(), M = quickin();
	for (int i = 0; i < M; ++i)
	{
		int a, b;
		a = quickin(), b = quickin();
		add(a, b), add(b, a);           // 存双向边,并编号配对 
	}
	
	tarjan(1, -1);
	
	for (bridge &e : ans)
	{
		if (e.x > e.y)    swap(e.x, e.y);
	}
	
	sort(ans.begin(), ans.end(), cmp);
	for (bridge e : ans)
		cout << e.x << ' ' << e.y << endl;
			
	return 0;
}

链式前向星

#include <bits/stdc++.h>

using namespace std;
typedef long long ll;

ll quickin(void)
{
	ll ret = 0;
	bool flag = false;
	char ch = getchar();
	while (ch < '0' || ch > '9')
	{
		if (ch == '-')    flag = true;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9' && ch != EOF)
	{
		ret = ret * 10 + ch - '0';
		ch = getchar();
	}
	if (flag)    ret = -ret;
	return ret;
}

struct edge{int to, next;};
struct bridge{int x, y;};

const int MAX = 153;
int N, M;
// 链式前向星 
edge E[5010];         // 存边 
int H[MAX], id = 1;   // H[] -- 以u为起点,最后插入的边(头插法) ; id -- 给边编号(从23开始) 
int dfn[MAX], low[MAX], tot;
vector<bridge> ans;

void add(int u, int v)
{
	E[++id] = {v, H[u]};   // 头插法(从23开始编号) 
	H[u] = id;
}

void tarjan(int u, int in_edge)             // in_edge -- 进入u的边的编号 
{
	// 入u,盖戳 
	dfn[u] = low[u] = ++tot;
	// 遍历u为起点的边,i为边的编号 
	for (int i = H[u]; i; i = E[i].next)
	{
		int v = E[i].to;
		if (!dfn[v])                          // 未访问v 
		{
			tarjan(v, i);
			low[u] = min(low[u], low[v]);     // 返回u,更新low 
			
			if (low[v] > dfn[u])              // 桥的条件 
				ans.push_back(bridge{u, v});
		}
		else if (i != (in_edge ^ 1))          // 已访问v,且不是反边 
			low[u] = min(low[u], dfn[v]);
	}
}

bool cmp(const bridge &a, const bridge &b)
{
	if (a.x == b.x)    return a.y < b.y;
	return a.x < b.x; 
}

int main()
{
	#ifdef LOCAL
	freopen("test.in", "r", stdin);
	#endif
	
	N = quickin(), M = quickin(); 
	for (int i = 0; i < M; ++i)
	{
		int a, b;
		a = quickin(), b = quickin();
		add(a, b), add(b, a);
	}
	
	tarjan(1, -1);
	
	for (bridge &e : ans)
	{
		if (e.x > e.y)    swap(e.x, e.y);
	}
	
	sort(ans.begin(), ans.end(), cmp);
	for (bridge e : ans)
		cout << e.x << ' ' << e.y << endl;
	
	return 0;
}

边双连通分量(eDCC)

eDCCedge Double Connected Component

  • 定义:无向图中极大不包含割边的连通块。
struct edge{int to, next;};

const int N = 1e4;    // 节点数 
const int M = 1e4;    // 边数 
edge E[M];
int H[N], id = 1;   // 采用链式前向星存边 
int dfn[N], low[N], tot;
stack<int> S; 
bool bri[M];            // bri[i] -- 编号为i的边是否是桥 
int dcc[N], d[N], cnt;  // dcc[u] -- u所在的eDCC; d[u] -- 缩点后u的度; cnt -- eDCC的数量 

void add(int u, int v)
{
	E[++id] = {v, H[u]};  // 头插法 
	H[u] = id;
}

void tarjan(int u, int in_edge)
{
	// 入u,盖戳,入栈 
	dfn[u] = low[u] = ++tot;
	S.push(u);
	
	for (int i = H[u]; i; i = E[i].next)
	{
		int v = E[i].to;
		if (!dfn[v])
		{
			tarjan(v, i);
			low[u] = min(low[u], low[v]);
			
			if (low[v] > dfn[u])             // 判断桥 
				bri[i] = bri[i ^ 1] = true;
		}
		else if (i != (in_edge ^ 1))
			low[u] = min(low[u], dfn[v]);
	}
	
	if (dfn[u] == low[u])   // eDCC的根节点的条件 
	{
		++cnt;
		int t;
		do
		{
			t = S.top();
			S.pop();
			dcc[t] = cnt;
		} while (t != u);
	}
}

点双连通分量(vDCC)

vDCCvertex Double Connected Component

  • 点双连通图:一张无向连通图,不存在割点
  • 点双连通分量:无向图的极大点双连通子图
#include <bits/stdc++.h>

using namespace std;

const int N = 1e4;
const int M = 1e4;
int n, m; 
vector<int> G[N], nG[N];    // G[] -- 原图; nG[] -- 新图 
int dfn[N], low[N], tot;
stack<int> S;
bool cut[N];                // 是否是割点 
vector<int> dcc[N];         // dcc[i] -- 编号为i的vDCC有哪些点 
int root, cnt, num, id[N];  // root -- 根节点; cnt -- vDCC个数; num -- 新图中点的个数; id[u] -- 割点u在新图中的编号 

void tarjan(int u)
{
	// 入u,盖戳,入栈 
	dfn[u] = low[u] = ++tot;
	S.push(u);
	// 特判孤立点 
	if (!G[u].size())
	{
		dcc[++cnt].push_back(u);
		return;
	}
	// 遍历u的边	
	int child = 0;
	for (int v : G[u])
	{
		if (!dfn[v]) // 未访问v 
		{
			tarjan(v);
			low[u] = min(low[u], low[v]); // 回u,更新low 
			
			if (low[v] >= dfn[u]) // 判割点 
			{
				++child;
				if (u != root || child > 1)
					cut[u] = true;
				
				++cnt;
				int t;
				do
				{
					t = S.top();
					S.pop();
					dcc[cnt].push_back(t);
				} while (t != v); // 到v停止 
				dcc[cnt].push_back(u); // 割点也要加入 
			}
		}
		else // 已访问v 
			low[u] = min(low[u], dfn[v]);
	}
}

int main()
{
	#ifdef LOCAL
	freopen("test.in", "r", stdin);
	#endif
	
	cin >> n >> m;
	for (int i = 0; i < m; ++i)
	{
		int a, b;
		cin >> a >> b;
		G[a].push_back(b);
		G[b].push_back(a);
	}
	
	for (int i = 1; i <= n; ++i)
		if (!dfn[i])
		{
			root = i;
			tarjan(i);
		}
	// 为每一个割点分配一个编号,从cnt+1开始 
	num = cnt;
	for (int i = 1; i <= n; ++i)
		if (cut[i])    id[i] = ++num;
	// 建立新图,将vDCC与对应割点连边 
	for (int i = 1; i <= cnt; ++i)
	{
		for (int j : dcc[i])
		{
			if (cut[j])
			{
				nG[i].push_back(id[j]);
				nG[id[j]].push_back(i);
			}
		}
	}
	
	return 0;
}

posted @ 2024-05-11 18:33  ltign  阅读(23)  评论(0编辑  收藏  举报