强连通分量

强连通分量

dfs 森林

将原有的有向边按照搜索的情况分为四类:

  • 树边 (tree edge):访问节点走过的边
  • 返祖边 (back edge):指向祖先节点的边
  • 横叉边 (cross edge):右子树指向左子树的边
  • 前向边 (forward edge):指向子树中节点的边

返祖边和树边必定构成环,横叉边和树边可能构成环,前向边不影响强连通分量

tarjan算法

一遍 dfs 维护强连通分量相关信息
复杂度 $O(n + m) $

Kosaraju算法

DAG 出栈顺序是反图的拓扑序
有向图 SCC 缩点成 DAG
最后一个出栈的点就是反图源点
故,tarjan 的 scc 编号是反序的拓扑序

对原图和反图均做一次 dfs 求得强连通分量

相关资料

https://www.cnblogs.com/dx123/p/16320476.html
oi wiki - scc

板子 - tarjan

struct SCC{//Strongly Connected Components
	int n;
	int idx, cnt;
	vector<int> stk;
	vector<int> dfn, low, bel;
	vector<vector<int>> scc, e;
	SCC(){}
	SCC(int x) : n(x){
		e.assign(n + 1, {});
		dfn.assign(n + 1, 0);
		low.assign(n + 1, 0);
		bel.assign(n + 1, 0);
		stk.clear(); scc.clear();
		idx = cnt = 0;
	}

	void add_edge(int u, int v){
		e[u].push_back(v);
		return ;
	}

	void tarjan(int u){
		dfn[u] = low[u] = ++ idx;
		stk.push_back(u);
		for(auto v : e[u]){
			if(!dfn[v]){
				tarjan(v);
				low[u] = min(low[u], low[v]);
			}else if(bel[v] == 0){
				low[u] = min(low[u], dfn[v]);
			}
		}
		if(dfn[u] == low[u]){
			++ cnt;
			vector<int> t;
			int v;
			do{
				v = stk.back();
				t.push_back(v);
				bel[v] = cnt;
				stk.pop_back();
			}while(v != u);
			scc.push_back(t);
		}
		return ;
	}

	void work(){
		for(int i = 1; i <= n; ++ i){
			if(!dfn[i]) tarjan(i);
		}
		return ;
	}
};

例题

综合运用

利用强连通分量缩点

当强连通分量个数为 1 时(需特判!!!)
子任务 A:1
子任务 B:0
因为此时整个图是强连通的

当强连通分量个数为 1 时
子任务 A:答案即为入度为 0 的点的个数
子任务 B:答案即为 max(入度为 0 的点数,出度为 0 的点数)。
因为想要将多个 DAG 变成一个 SCC,则最划算的就是将无出度点连向无入度点,为了连接尽可能少的边,从 0 出度点开始走,依次走过 0 入度点、0出度点、0 入度点、0出度点、... 直至某种点数不够,则将其连至已有的另一种点,剩下的也是如此。这样,就可以得到一个 SCC,所需加边数即为 max(入度为 0 的点数,出度为 0 的点数)
Qiansui_code

还得是洛谷评论区,orz!
什么时候婚姻是不安全的,以第一对夫妻为例,如果说,可以有下面这种情况成立,即男 1 找女 2,男 2 找女 3,男 3 找女 4, ...,男 k 找女 1 时,构成闭环,剩下如果还有夫妻则保持不变,此时婚姻是不安全的。如果说最终无法实现,说明婚姻就是安全的。
有没有种强连通分量的感觉,如果说一对夫妻均在一个 SCC 里面,则婚姻就是不安全的;反之安全(感觉没有说清楚,再看看题解想一想)

做法:抽象建图,对于夫妻关系连边女 -> 男,对于情人关系连边男 -> 女,在利用 tarjan 缩点,记录每个点属于哪一个 SCC。如果夫妻二人属于一个 SCC,则不安全;反之安全

Qiansui_code


模板题

判断整个图是否强连通 - hdu 1269 迷宫城堡

判断强连通分量是否只有一个即可


求强连通分量个数 - 洛谷 P2863 [USACO06JAN] The Cow Prom S

代码 - tarjan

//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x, y, sizeof(x))
#define debug(x) cout << #x << " = " << x << '\n'
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << '\n'
//#define int long long
 
using namespace std;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<ull, ull> pull;
typedef pair<double, double> pdd;
/*
模板题
*/
const int maxm = 1e4 + 5, inf = 0x3f3f3f3f, mod = 998244353;
int n, m;
vector<int> e[maxm];
 
int dfn[maxm], low[maxm], tot = 0;
bool instk[maxm];
stack<int> stk;
vector<vector<int>> scc;

void tarjan(int u){
	dfn[u] = low[u] = ++ tot;
	instk[u] = true;
	stk.push(u);
	for(auto v : e[u]){
		if(!dfn[v]){
			tarjan(v);
			low[u] = min(low[u], low[v]);
		}else if(instk[v]){
			low[u] = min(low[u], dfn[v]);
		}
	}
	if(dfn[u] == low[u]){
		vector<int> c;
		int v;
		do{
			v = stk.top();
			c.push_back(v);
			instk[v] = false;
			stk.pop();
		}while(v != u);
		scc.push_back(c);
	}
	return ;
}

void solve(){
	cin >> n >> m;
	for(int i = 0; i < m; ++ i){
		int u, v;
		cin >> u >> v;
		e[u].push_back(v);
	}
	for(int i = 1; i <= n; ++ i){
		if(!dfn[i]) tarjan(i);
	}
	int ans = 0;
	for(auto c : scc){
		if(c.size() > 1) ++ ans;
	}
	cout << ans << '\n';
	return ;
}

signed main(){
	ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
	int _ = 1;
	// cin >> _;
	while(_ --){
		solve();
	}
	return 0;
}

强连通分量缩点 - 洛谷 P2341 [USACO03FALL / HAOI2006] 受欢迎的牛 G

题意
给定有向图,判断图中有多少个点满足图中剩余的点均可达该点

思路
利用强连通分量缩点,再判断是否仅有一个连通块出度为 0
强连通分量缩点,整图变为 DAG,再看是否存在唯一汇点

代码

//>>>Qiansui
#include<bits/stdc++.h>
#define ll long long
#define ull unsigned long long
#define mem(x,y) memset(x, y, sizeof(x))
#define debug(x) cout << #x << " = " << x << '\n'
#define debug2(x,y) cout << #x << " = " << x << " " << #y << " = "<< y << '\n'
//#define int long long

using namespace std;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef pair<ull, ull> pull;
typedef pair<double, double> pdd;
/*

*/
const int maxm = 1e4 + 5, inf = 0x3f3f3f3f, mod = 998244353;
int n, m;
vector<int> e[maxm];

int dfn[maxm], low[maxm], bel[maxm], sz[maxm], cnt = 0, idx = 0;
bool instk[maxm];
stack<int> stk;

void tarjan(int u){
	dfn[u] = low[u] = ++ idx;
	instk[u] = true;
	stk.push(u);
	for(auto v : e[u]){
		if(!dfn[v]){
			tarjan(v);
			low[u] = min(low[u], low[v]);
		}else if(instk[v]){
			low[u] = min(low[u], dfn[v]);
		}
	}
	if(dfn[u] == low[u]){
		++ cnt;
		while(true){
			++ sz[cnt];
			int v = stk.top();
			bel[v] = cnt;
			instk[v] = false;
			stk.pop();
			if(v == u) break;
		}
	}
	return ;
}

void solve(){
	cin >> n >> m;
	for(int i = 0; i < m; ++ i){
		int u, v;
		cin >> u >> v;
		e[u].push_back(v);
	}
	for(int i = 1; i <= n; ++ i){
		if(!dfn[i]) tarjan(i);
	}
	int cnts = 0, cntv = 0;
	vector<int> outd(n + 1, 0);
	for(int u = 1; u <= n; ++ u){
		for(auto v : e[u]){
			if(bel[u] != bel[v]) ++ outd[bel[u]];
		}
	}
	for(int i = 1; i <= cnt; ++ i){
		if(outd[i] == 0){
			++ cnts;
			cntv += sz[i];
		}
	}
	if(cnts > 1) cout << "0\n";
	else cout << cntv << '\n';
	return ;
}

signed main(){
	ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
	int _ = 1;
	// cin >> _;
	while(_ --){
		solve();
	}
	return 0;
}

SCC + DP - 洛谷 P2272 [ZJOI2007] 最大半连通子图

tarjan 的 scc 编号是反序的拓扑序!
利用 SCC 缩点,再在新的 DAG 上跑 DP(简单DP,找最长路的长度和条数)
每一个点都记下自己所能抵达的最长链的长度和方案数
代码:Qiansui_code


tarjan + DP - 洛谷 P3627 [APIO2009] 抢掠计划

利用 tarjan 将每一个强连通分量分别考虑,再做 DP
仅需要从起点开始跑 tarjan 即可
DP 初值为负的足够大,这样判断当前强连通分量中是否存在酒吧即判断当前 DP 值是否需要赋 0
代码:Qiansui_code


tarjan + DP 洛谷 P2403 [SDOI2010] 所驼门王的宝藏

DP 思路依旧是 SCC 缩成 DAG,在 DAG 上跑 DP 统计最长链
但是如果按照题意建图,需要 $O(n ^ 2) $ 条边,故可以对于一行的边找一个代理,所有该行的点均和这个代理点连接,对于所有行所有列均如此操作,边数可以降到 $O(n) $
之后就是在求强连通分量时做 DP,注意一点,因为有代理点的存在,故在最终统计时代理点对最终答案不产生贡献
代码:Qiansui_code

posted @ 2023-08-22 15:28  Qiansui  阅读(64)  评论(0)    收藏  举报