边三联通分量

感觉口胡了很多遍的模板算法,快 NOI 了才想起来写写代码。其实边三的代码很好写,网上许多资料都写麻烦了。

边联通性其实是一个很能扩展的东西。两个点之间如果最少要割开 \(k\) 条边才能使它们之间不联通,称这两个点的边联通度为 \(k\)。称两个点之间是 \(k\) 边联通的,当且仅当这两个点的边联通度 \(\ge k\)

边联通性具有很好的传递性。即若 \(x,y\)\(k\) 边联通的,\(y,z\)\(k\) 边联通的,那么 \(x,z\)\(k\) 边联通的。证明考虑假设一个大小 \(<k\) 的边集割开了 \(x,z\),那么这个割集势必没有割开 \(x,y\)\(y,z\),此时 \(x,z\) 仍联通矛盾。\(k\) 边联通分量就是把 \(k\) 边联通的点连边形成的连通块。由上面的传递性我们可以知道边联通分量中的点两两 \(k\) 边联通。

这件事告诉我们了 \(k\) 边联通分量总是可以被良好定义且结构优美的,但是点联通分量光是在 \(k=2\) 的时候就略显丑陋——有可能一个点在多个点双中。

如何判断 \(k\) 边联通性呢?我们考虑一个比较通用的问题:给定一张图的 \(k\) 条边,问其是否是边割集。

做法是考虑经典的割空间与环空间。极小的边割集定义为对于一个边割集加回其中一条边后,图的联通性有变化,或者说你考虑把图仍以分成两个部分,那么跨过这两个部分的边组成一个极小的边割集。一个图的割空间是一个由所有极小的边割集组成的线性空间,即其在对称差(异或)运算下封闭。一个图的回路空间也是线性空间(回路指每个连通块都有欧拉回路的边集,也就是每个点均与其中的偶数条边相邻)。这两个空间互为正交补,即边割集与回路的公共部分一定大小为偶数,且与一个回路公共部分大小为偶数的一定是边割集。

由一些经典结论我们知道只用非树边和树边形成的简单环异或就可以得到所有的环。所以判定一个边集是不是极小的边割集可以异或哈希给非树边随机赋权,然后让树边的权值等于所有覆盖它的非树边权值的异或。这样如果一个边集权值异或和为 0,说明其与某个回路正交。

判断一个集合是否是边割集就是看存不存在一个非空子集是极小边割集,那么就是问这个集合的权值是不是线性相关。

现在对于 \(k=3\) 的情况,我们应用这个技巧给边随机赋权。那么一个点集边三联通相当于其中所有边的大小不超过三的子集线性不相关。线性相关无非这么几种情况:

  • 存在一条树边边权为 0。即这条边是割边,在三联通分量中我们需要割开这条树边。

  • 存在一个树边和非树边权值相等。我们也需要割开这条树边。

  • 存在两条树边权值相等。此时中间的部分需要跟两边的部分隔开。

发现我们要做一个分割连通块的操作,一种方法是 dfs 打标记,但这样细节还是不够少。我发现 tzc_wk 博客里的方法是又好写又好记。我们同样采用异或哈希的手法,一个连通块需要跟其它部分分开相当于这个连通块需要全体异或上一个与众不同的值。那么对于前两种情况,打一个子树异或标记,对于第三种一上一下打两个子树异或标记,就可以区分出中间的连通块。

由于是 dfs 树,非树边一定直上直下,那么所有权值相等的树边肯定排列在一条直上直下的树链上,显然我们只需要处理这条链上相邻的两条边就够了。我们只需要开个全局 Hash 表,在 dfs 回溯时找到子树中第一条跟它权值相同的边处理第三种情况。

只需要两次 dfs。如果你选择多跑一次 tarjan 处理第一种情况有点多此一举了。

#include <cstdio>
#include <random>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/hash_policy.hpp>
using namespace std;
mt19937_64 rng(random_device{}());
typedef unsigned long long ull;
int read(){
	char c=getchar();int x=0;
	while(c<48||c>57) c=getchar();
	do x=x*10+(c^48),c=getchar();
	while(c>=48&&c<=57);
	return x;
}
int n,m;
const int N=500003;
int hd[N],ver[N<<1],nxt[N<<1],tot=1;
void add(int u,int v){nxt[++tot]=hd[u];hd[u]=tot;ver[tot]=v;}
int dfn[N],num;
ull w[N],eq[N];
typedef vector<int> vi;
__gnu_pbds::gp_hash_table<ull,int> mp,id;
__gnu_pbds::gp_hash_table<ull,bool> exi;
vector<vi> res;
bool ontr[N<<1],rt[N];
void dfs(int u,int las){
	dfn[u]=++num;
	for(int i=hd[u];i;i=nxt[i]){
		int v=ver[i];
		if(i==las) continue;
		if(dfn[v]){
			if(dfn[v]<dfn[u]){
				ull val=rng();
				w[u]^=val;
				w[v]^=val;
				exi[val]=1;
			}
		}
		else{
			ontr[i]=1;
			dfs(v,i^1);
			w[u]^=w[v];
		}
	}
	if(exi.find(w[u])!=exi.end()) eq[u]^=rng();
	else{
		auto it=mp.find(w[u]);
		if(it!=mp.end()){
			ull val=rng();
			eq[it->second]^=val;
			eq[u]^=val;
			it->second=u;
		}
		else mp[w[u]]=u;
	}
}
void split(int u){
	for(int i=hd[u];i;i=nxt[i])
		if(ontr[i]){
			int v=ver[i];
			eq[v]^=eq[u];
			split(v);
		}
	if(id.find(eq[u])!=id.end()) 
		res[id[eq[u]]].emplace_back(u);
	else{
		id[eq[u]]=res.size();
		res.emplace_back(1,u);
	}
}
int main(){
	n=read();m=read();
	for(int i=1;i<=m;++i){
		int u=read(),v=read();
		add(u,v);add(v,u);
	}
	exi[0]=1;
	for(int i=1;i<=n;++i)
		if(!dfn[i]){
			eq[i]^=rng();
			dfs(i,0);
			rt[i]=1;
		}
	for(int i=1;i<=n;++i) if(rt[i]) split(i);
	for(vi &cur:res) sort(cur.begin(),cur.end());
	sort(res.begin(),res.end());
	printf("%lu\n",res.size());
	for(vi cur:res){
		for(int x:cur) printf("%d ",x);
		putchar('\n');
	}
	return 0;
}
posted @ 2024-07-03 09:21  yyyyxh  阅读(20)  评论(0编辑  收藏  举报