首页 > 其他分享 >tarjan 学习笔记

tarjan 学习笔记

时间:2023-07-15 23:56:41浏览次数:34  
标签:tarjan int cnt 笔记 学习 Dfn MAXN Low push

tarjan 学习笔记

  1. 求解强联通分量

我们从一个点开始建立 dfs 树,有如下四种边:

  • 树边

若 \(u\) 到 \(v\) 有边,且满足 \(v\) 没有被访问过,则这条边为树边

  • 返祖边

若 \(u\) 到 \(v\) 有边,且满足 \(v\) 已被访问过,则这条边为返祖边

  • 横叉边

若 \(u\) 到 \(v\) 有边,且满足 \(u\) 和 \(v\) 不是儿子祖先关系,则这条边为横叉边

  • 没用边

若 \(u\) 到 \(v\) 有边,且满足 \(u\) 可以通过一定的路径到 \(v\),则这条边没有任何作用

如下图为一个dfs树:黑色为树边,红色为返祖边,蓝色为横叉边,紫色为没用边

我们根据这些边求解强联通

明显的,没用边没用,我们考虑横叉边有没用

由于是 dfs ,所以横叉边一定是后访问的点到先访问的点,那是因为如果前访问的点到后访问的点,那这条边即为树边

所以横叉边必定是单向边

树边一定是往下的,所以也构不成强联通,那么只有返祖边满足了

如果一个边要到达另一个不是自己儿子的点,那么只能通过返祖边通过祖先做跳板了

那我们的任务就为:探究一个点能往上走多少个点

我们维护两个数组分别为:

  • \(dfn_x\) 表示 \(x\) 是 dfs 第 \(dfn_x\) 访问到的点,也就是 dfs 序
  • \(low_x\) 表示 \(x\) 内所有子树,包括它自己能够到达的 \(dfn\) 最小的点

我们开始分析代码步骤:

假设 \(u\) 为当前节点,\(v\) 为 \(u\) 可以到的边

  • \(u->v\) 为树边

递归 \(v\) 之前更新 \(low\) 数组:\(low_u=\min(low_u,low_v)\)

  • \(u->v\) 为横叉边

通常,这种边是可以不用管的,但是当 \(v\) 连的祖先可以到 \(u\),这就可以更新,那怎么判断呢?

我们开个,里面存的是强联通分量的点,注意:同一个强联通分量在一起,如果这个强联通分量找完了,应该弹出里面 \(low\) 为最顶上的点

因为 \(v\) 在 \(u\) 之前被访问,所以 \(v\) 肯定是遍历过的,若果它在栈中,就说明它所在的强联通分量还没找完,也就是 \(u,v\) 在同一个强联通分量里面,这就可以更新 \(low\) 数组:\(low_u=\min(low_u,low_v)\)

  • \(u->v\) 为返祖边

容易发现,这种情况在上种情况已经考虑进去了,不必再次考虑

最后,当遍历完所有儿子之后,发现 \(low_u=dfn_u\) 就说明这个为强联通的头,弹出所有 \(low=u\) 的点即可

Code

#include <bits/stdc++.h>

using namespace std;

const int MAXN = 1e5 + 7, MAXM = 1e6 + 7;

struct Node {
	int nxt, to;
}Edge[MAXM];

int H[MAXN], E_cnt;

void add(int from, int to) {
	Edge[++ E_cnt] = Node{H[from], to};
	H[from] = E_cnt;
}

int n, m, Value[MAXN];

int Dfn[MAXN], Low[MAXN], Scc[MAXN], scc, cnt;

bool V[MAXN];

int Sum[MAXN];

stack <int> T;

void tarjan(int u) {
	Dfn[u] = ++ cnt; Low[u] = Dfn[u]; 
	V[u] = true; T.push(u);
	for (int i = H[u]; i; i = Edge[i].nxt) {
		int v = Edge[i].to;
		if (!Dfn[v]) tarjan(v), Low[u] = min(Low[u], Low[v]);
		else if (V[v]) Low[u] = min(Low[u], Low[v]);
	} 
	if (Dfn[u] == Low[u]) {
		Scc[u] = ++ scc; Sum[scc] = Value[u];
		while (!T.empty() && T.top() != u) V[T.top()] = 0, Scc[T.top()] = scc, Sum[scc] += Value[T.top()], T.pop();
		T.pop(); V[u] = 0;
	}
}

int X[MAXM], Y[MAXM], In[MAXN], ans, Dp[MAXN];

int topu() {
	queue <int> Q;
	for (int i = 1; i <= scc; i ++) {
		if (!In[i]) Q.push(i);
	}
	while (!Q.empty()) {
		int now = Q.front(); Q.pop();
		Dp[now] += Sum[now];
		for (int i = H[now]; i; i = Edge[i].nxt) {
			int v = Edge[i].to;
			Dp[v] = max(Dp[v], Dp[now]);
			if (-- In[v] == 0) Q.push(v);
		}
		ans = max(ans, Dp[now]);
	}
	return ans;
}

int main () {
	cin >> n >> m;
	for (int i = 1; i <= n; i ++) cin >> Value[i];
	for (int i = 1; i <= m; i ++) {
		cin >> X[i] >> Y[i]; add(X[i], Y[i]);
	}
	for (int i = 1; i <= n; i ++)
		if (!Dfn[i]) tarjan(i);
	
	memset(H, 0, sizeof(H)); E_cnt = 0;
	for (int i = 1; i <= m; i ++) {
		int x = Scc[X[i]], y = Scc[Y[i]];
		if (x == y) continue;
		add(x, y); In[y] ++;
	}
	cout << topu();
	return 0;
}

2.求解边双联通分量

因为在无向图里,所以没有横叉边了

我们到这里可以发现,SCC和边双联通本质上就是通过 tarjan 求环,所以代码基本无异

注意一点:如果一条边走了,那么这条班边的反向边也不能走了

还需要改的地方为:因为没有横叉边,那么用于判断横叉边的标记数组 \(V\) 就没必要存在了

Code

#include <bits/stdc++.h>

using namespace std;

const int MAXN = 5e5 + 7, MAXM = 2e6 + 7;

struct Node {
	int nxt, to;
}Edge[2 * MAXM];

int H[MAXN], E_cnt = 1;//注意 

void add(int from, int to) {
	Edge[++ E_cnt] = Node{H[from], to};
	H[from] = E_cnt;
}

int n, m;

int Dfn[MAXN], Low[MAXN], cnt;

stack <int> T;

vector <vector<int> > Ans;

void tarjan(int u, int e) {
	Dfn[u] = ++ cnt; Low[u] = Dfn[u]; T.push(u);
	for (int i = H[u]; i; i = Edge[i].nxt) {
		if (i == (e ^ 1)) continue;
		int v = Edge[i].to;
		if (!Dfn[v]) tarjan(v, i), Low[u] = min(Low[u], Low[v]);
		else Low[u] = min(Low[u], Low[v]);
	} 
	if (Dfn[u] == Low[u]) {
		vector <int> v; v.push_back(u);
		while (!T.empty() && T.top() != u) v.push_back(T.top()), T.pop();
		T.pop(); Ans.push_back(v);
	}
}

int main () {
	cin >> n >> m;
	for (int i = 1, x, y; i <= m; i ++) {
		cin >> x >> y, add(x, y); add(y, x);
	}
	for (int i = 1; i <= n; i ++)
		if (!Dfn[i]) tarjan(i, 0);
	
	cout << Ans.size() << '\n';
	for (auto w : Ans) {
		cout << w.size() << ' ';
		for (auto i : w) cout << i << ' ';
		cout << '\n';
	}
	return 0;
}

3.求解点双联通问题

我们发现一个性质:如果 \(a\) 和 \(b\) 点双联通,\(b\) 和 \(c\) 点双联通,则不一定有 \(a\) 和 \(c\) 点双联通

根据这个性质,我们发现,一个点可以同时被编进多个点双联通,这也就导致我们不能像前两个一样对他进行普通弹栈

先考虑怎么求割点

我们分两种情况:

  • \(u\) 不为根节点

把 \(u\) 放进 dfs 上,如果它不为根节点,则它是把子树和自己的祖先割开了,则一定满足如下不等式:

\[low_v\ge dfn_u \]

则如果满足这个,这个点为割点

  • \(u\) 为根节点

如果 \(u\) 有1个孩子以上,那么 \(u\) 一定为割点

好,我们割点求出来了,看看怎么求点双联通

不难发现,每个点双之间的分割点就是割点,如果我们找到了一个割点,就把栈中和它是一个环的弹出来,但是注意不要弹出它本身!!

为什么能这样做呢?那是因为除了第一个点双其他的点双都是以割点为顶点的,如下图:

容易发现除了起点 1,2,3,4 不以 4 为顶点,其他的点双都以 4 为顶点

所以我们就可以进行上述操作

注意,假设 \(u\) 为顶点, \(u->v\) 为一条树边,回溯的时候,栈内存的是所有顶点为 \(u\) 的点双,那就会误判,所以要解决这个问题

我们发现,我们第一次找到的 \(v\) 是最先入栈的,所以启发我们,弹到 \(v\) 为至!

于是在弹栈的时候稍加处理,就可以了

但是还有一种情况我们没有考虑到,就是如果这个点为孤点咋办,直接加入点双即可

Code

#include <bits/stdc++.h>

using namespace std;

const int MAXN = 5e5 + 7, MAXM = 2e6 + 7;

struct Node {
	int nxt, to;
}Edge[2 * MAXM];

int H[MAXN], E_cnt = 1;//注意 

void add(int from, int to) {
	Edge[++ E_cnt] = Node{H[from], to};
	H[from] = E_cnt;
}

int n, m;

vector <vector <int> > Ans;

int Dfn[MAXN], Low[MAXN], cnt, rt;

stack <int> T;

void tarjan(int u, int e) {
	Dfn[u] = ++ cnt; Low[u] = Dfn[u]; T.push(u);
	int son = 0; 
	for (int i = H[u]; i; i = Edge[i].nxt) {
		if (i == (e ^ 1)) continue;
		int v = Edge[i].to;
		if (!Dfn[v]) {
			tarjan(v, i);
			son ++; Low[u] = min(Low[u], Low[v]);
			if (Low[v] >= Dfn[u]) {//注意这里是根也可以 
				vector <int> V; V.push_back(u);
				while (!T.empty() && T.top() != v) V.push_back(T.top()), T.pop();//注意
				V.push_back(T.top()); T.pop();
				Ans.push_back(V);//不用吧u弹掉 
			}	
		}
		else Low[u] = min(Low[u], Dfn[v]);//注意!! 
	} 
	if (u == rt && son == 0) {
		vector <int> V; V.push_back(u);
		Ans.push_back(V);
	}
}

int main () {
	cin >> n >> m;
	for (int i = 1, x, y; i <= m; i ++) {
		cin >> x >> y, add(x, y); add(y, x);
	}
	for (int i = 1; i <= n; i ++)
		if (!Dfn[i]) rt = i, tarjan(i, 0);
	cout << Ans.size() << '\n';
	for (auto w : Ans) {
		cout << w.size() << ' ';
		for (auto v : w) {
			cout << v << ' ';
		}
		cout << '\n';
	}
	return 0;
}

  • 扩展:圆方树

我们看下面那个图:

基本步骤就是:

  1. 求出点双
  2. 在每个点双内部构造一个方点
  3. 每个点双内部的点都连向这个方点
  4. 把全部边都删掉

于是乎,我们得到了一颗树,树有很好的性质,这颗树同样有很好的性质

由于是点双,所以它有跟点双一样的性质:点双内任意两点的路径并集为整个点双

我们来看一道题:[APIO2018] 铁人两项

题目要求有多少个三元组满足 \(<s,c,f>\) 满足 \(s->f\) 这条简单路径上有 \(c\)

我们固定 \(s\) 和 \(f\) 现尝试求出有多少个 \(c\)

明显的,\(c\) 的数量为 \(s->f\) 这条简单路径的并集的点,于是就有了一个思路:给圆方点复赋值!

既然我们要求 \(s->f\) 的简单路径的并集的点 - 2(不包括 \(s\) 和 \(f\)),那我们不妨给方点赋值为整个点双的大小,每个原点赋值为 \(-1\),那我们跑一遍 \(dfs\) 把 \(s\) 到 \(f\) 上的点,把路径和加起来就行啦

问题转换成统计圆方树上 \(\sum_{u,v\in G}u->v\),求路径实在太过麻烦,可以转换为求点对答案的贡献,注意要乘2,因为 \(s->t=t->s\)

注意:这里不用求孤点,因为它对答案毛贡献都没有

Code

#include <bits/stdc++.h>

using namespace std;

#define int long long

const int MAXN = 1e5 + 7;

int n, m;

vector <int> G[MAXN], T[MAXN * 2];//懒了 

int Dfn[MAXN], Low[MAXN], cnt, Val[2 * MAXN], dfc, num, ans;

int t[2 * MAXN], tp;

void tarjan(int u) {
	Dfn[u] = Low[u] = ++ dfc; t[++ tp] = u;
	num ++;
	for (auto v : G[u]) {
		if (!Dfn[v]) {
			tarjan(v);
			Low[u] = min(Low[u], Low[v]);
			if (Low[v] >= Dfn[u]) {
				Val[++ cnt] = 0;
				T[cnt].push_back(u); T[u].push_back(cnt); Val[cnt] ++;
				for (int x = 0; x != v; tp --) {
					x = t[tp];
					T[cnt].push_back(x); T[x].push_back(cnt);
					Val[cnt] ++;
				}//注意,这里将v也弹掉了 
			}
		} else Low[u] = min(Low[u], Dfn[v]);
	} 
}

int Vis[MAXN * 2], Siz[MAXN * 2];

void dfs(int u, int f) {
	Vis[u] = 1, Siz[u] = (u <= n);//注意
	for (auto v : T[u]) {
		if (v == f) continue;
		dfs(v, u); 
		ans += 2ll * (Val[u] * Siz[u] * Siz[v]);//子树内 
		Siz[u] += Siz[v];
	} 
	ans += 2ll * (Val[u] * Siz[u] * (num - Siz[u]));//子树外 
}
 
signed main () {
	cin >> n >> m;
	for (int i = 1; i <= n; i ++) Val[i] = -1;
	cnt = n;
	
	for (int i = 1, u, v; i <= m; i ++) {
		cin >> u >> v;
		G[u].push_back(v); G[v].push_back(u);
	}
	for (int i = 1; i <= n; i ++) 
		if (! Dfn[i]) {
			num = 0; tp --;
			tarjan(i); dfs(i, 0);
		}
	cout << ans << '\n';
	return 0;
}

标签:tarjan,int,cnt,笔记,学习,Dfn,MAXN,Low,push
From: https://www.cnblogs.com/Phrvth/p/17557258.html

相关文章

  • 强化学习:基于蒙特卡洛树和策略价值网络的深度强化学习五子棋(含码源)
    强化学习:基于蒙特卡洛树和策略价值网络的深度强化学习五子棋(含码源)特点自我对弈详细注释流程简单代码结构net:策略价值网络实现mcts:蒙特卡洛树实现server:前端界面代码legacy:废弃代码docs:其他文件utils:工具代码network.py:移植过来的网络结构代码model_5400.p......
  • 如何学习 左耳朵耗子
    目录知识图谱注重基础,基础要打牢使用知识图谱知识来源系统的学习这个技术出现的背景、初衷和要达到什么样的目标或是要解决什么样的问题。这个技术的优势和劣势分别是什么,或者说,这个技术的trade-off是什么。这个技术适用的场景。技术的组成部分和关键点。技术的底层原理和关键实......
  • 《架构整洁之道》学习笔记 Part 1 概述
    本书主题介绍什么是优秀的软件架构,以提高软件架构质量介绍系统架构的各种属性与成本和生产力的关系,以采用好的设计和架构以便减少构建成本好的软件架构可以带来什么?大大节省软件项目构建与维护的人力成本每次变更:改动少,易于实施,不容易出bug用最小的成本,最大程度满足功能......
  • Python学习3
    Python学习11Python列表11.1Python集合(数组)Python编程语言中有四种集合数据类型:列表(List)是一种有序和可更改的集合。允许重复的成员。元组(Tuple)是一种有序且不可更改的集合。允许重复的成员。集合(Set)是一个无序和无索引的集合。没有重复的成员。词典(Dictionary)是......
  • 重庆摩托车多的原因(研究性学习
    独特的地理位置重庆是一座山城,主城大部分地区都是山地,海拔高差达到2723.7米。这里的建筑都是依山而建,并且密度很大,导致重庆的道路不会很平坦,也相对狭窄。骑摩托车可以在大街小巷中穿梭,摩托车较小的体型可以方便地通过这些地形复杂的区域,令人们的出行变得更加方便快捷。企业与社......
  • Win32学习
    1、导入关于Win32的错误认知:(1)已经有malloc()函数了,为什么还要学Win32API?(2)学MFC就可以了,为什么要学Win32?Win32课程包含的内容:01、字符09、文件系统02、多线程10、内存映射03、线程同步11、DLL04、窗口的本质12、远程注入05、Windows消息机制13、模块隐藏06、子窗......
  • Java学习day04: 方法和数组
    我在B站上大学......
  • 太爆了!阿里最新出品2023版JDK源码学习指南,Github三天已万赞
    近后台收到很多粉丝私信,说的是程序员究竟要不要去读源码?当下行情,面试什么样的薪资/岗位才会被问到源码?对此,我的回答是:一定要去读,并且要提到日程上来!据不完全统计,现在市面上不管是初级,中级,还是高级岗,面试的时候都有可能会问到源码中的问题,它已经成为程序员常规必备的一个技术点。如......
  • Ubuntu学习:获取ip地址
    参考:https://www.howtouseubuntu.com/network/ubuntu-command-terminal-find-ip-address-in-ubuntu/当使用命令行在Ubuntu系统上获取IP地址时,以下是几个示例:使用ip命令获取IP地址:$ipaddrshow示例输出:1:lo:<LOOPBACK,UP,LOWER_UP>mtu65536qdiscnoqueuestateUNKN......
  • 学习java第3天
    计算机语言发展史第一代语言机器语言:二进制第二代语言汇编语言应有:逆向工程机器人病毒第三代语言摩尔定律高级语言:c语言c++语言Java语言c#语言·······Java的诞生1972年c诞生贴近硬件,运行快,效率高操作系统,编辑器,数据库1982年c++诞生面向对象,......