双连通分量学习笔记+杂题

图论系列:

前言:

もしも明日がくるのなら
あなたと花を育てたい
もしも明日がくるのなら
あなたと愛を語りたい
走って 笑って 転んで

相关题单:https://www.luogu.com.cn/training/641352

一.割点与桥

双连通分量是针对无向图来说的,有向图是强连通分量。在了解双连通分量之前需要先了解无向图中的割点与桥。

1.割点:

(1)定义:

割点:对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点(又称割顶)。

如下图中的点 2 就是一个割点,因为删去点 2 之后原图就变为 2 个连通分量了。

(2)求法:

一般使用 Tarjan 算法求解(因为一个一个判断时间复杂度太高了,只能利用一些性质)还是考虑同强连通一样,在无向图中跑一个 dfs 生成树,维护两个数组。

dfn[x] :表示点 xdfs 序(也就是访问的先后顺序)。

low[x]:表示满足以下条件的点的 dfn 最小值:

  • x 的子树内。

  • 经过一条不是树边的边,能够从 x 的子树内的点到达的点。

统计出 dfn,low 数组之后,由于在一个 dfs 树内,dfnx 始终小于等于 x 子树内的点的 dfn 值,所以判断一个点是否为割点的条件就是存在一个儿子 v 使得 dfnulowv 。因为此时儿子 v 之下的整个子树没有边可以连接 u 子树外的点了,断开 uv 这条边之后,连通分量的数量必然 +1。

但是对于 dfs 生成树的起点不适用,对于 dfs 生成树的起点只需要判断其有几个不互相连通的儿子即可,只要有两个互相不连通的儿子,dfs 生成树的起点就是割点。

模板题代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack>
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e5+5;
int n,m,num,root;
int dfn[M],low[M];
bool vis[M];

int cnt=0;
struct N{
	int to,next;
}; N p[M<<1];
int head[M];
inline void add(int a,int b)
{
	++cnt;
	p[cnt].next=head[a];
	head[a]=cnt;
	p[cnt].to=b;
}
inline void dfs(int u,int f)
{
	dfn[u]=low[u]=++num;//初始化dfn,low数组
	int siz=0;
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(v==f) continue;
		if(!dfn[v])
		{
			dfs(v,u),++siz;//每一次遍历到这,就说明不连通的儿子数+1,因为如果当前这个儿子与另外一个已经遍历过的儿子连通了,那么那个儿子会优先将这个点遍历,而不会留给根节点来遍历
			low[u]=min(low[u],low[v]);//儿子们能到的点,当前点也可以到达
			if(low[v]>=dfn[u]&&u!=root) vis[u]=1;//如果满足条件并且不是 dfs 生成树的根节点
		}
		else low[u]=min(low[u],dfn[v]);//经过一条不是树边的边,能够到达的 dfn 值最小的点
	}
	if(u==root&&siz>1) vis[u]=1;//特判根节点
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1,a,b;i<=m;++i) cin>>a>>b,add(a,b),add(b,a);//无向图
	for(int i=1;i<=n;++i)
	{
		if(!dfn[i]) root=i,dfs(i,0);//没保证是个无向连通图
	}
	int ans=0;
	for(int i=1;i<=n;++i) ans+=vis[i];
	cout<<ans<<"\n";
	for(int i=1;i<=n;++i) if(vis[i]) cout<<i<<" ";
	return 0;
}

2.割边:

(1)定义:

割边:和割点差不多,叫做。对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边。

如下图中的红色的边就是割边,因为删去 12 这条边之后原图变为两个连通分量。

(2)求法:

和割点差不多,只要改一处:判断条件dfnu<dfnv,而且不需要考虑根节点的问题。

修改判断的理由是可能出现重边。以上图为例,如果 12 这条边有两条,那么删去其中一条便不会增加连通分量数。但是由于是无向边,从父亲到自己有一条边,自己到父亲有一条边,既要消除这种影响,又要保留重边的影响。

那么我们对于每一条边赋一个边权 w,简单点就是记录一下当前边是第几条边,然后再 dfs 无向图的时候,记录一下当前点是由那条边转移过来的,设为 pre,如果自己有一条出边的边权与 pre 相同,就说明这是同一条边,选择跳过。

代码:

inline void tarjan(int u,int pre)
{
	dfn[u]=low[u]=++num;
	s.push(u);
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(p[i].val==pre) continue;
		if(!dfn[v])
		{
			tarjan(v,p[i].val);
			low[u]=min(low[u],low[v]);
			if(low[v]>dfn[u]);//代表这条 u -> v 的边就是割边了,可能会进行一些操作,后面讲。
		}
		else low[u]=min(low[u],dfn[v]);
	}
}
//其余的同割点,不用特判根节点
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1,a,b;i<=m;i++)
	{
		cin>>a>>b;
		add(a,b,i),add(b,a,i);//给每条边赋一个边权
	}
	for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,0);
	return 0;
}

二.双连通分量

1.相关定义:

边双连通:在一张连通的无向图中,对于两个点 uv,如果无论删去哪条边(只能删去一条)都不能使它们不连通,我们就说 uv 边双连通。

点双连通:在一张连通的无向图中,对于两个点 uv,如果无论删去哪个点(只能删去一个,且不能删 uv 自己)都不能使它们不连通,我们就说 uv 点双连通。

边双连通分量:对于一个无向图中的极大边双连通的子图,我们称这个子图为一个 边双连通分量。

点双连通分量:对于一个无向图中的极大点双连通的子图,我们称这个子图为一个 点双连通分量。

2.性质:

(1)边双连通:

一个边双连通分量没有割边。(根据定义)

u,v 边双连通当且仅当 u,v 之间没有必经边。(根据定义)

由于边双连通分量是由一个个割边分隔开来,所以每一个点只属于一个边双连通分量

因为每个点只属于一个边双连通分量,所以边双连通具有传递性,若 x,y 边双连通,y,z 边双连通,则 x,z 边双连通。

将每个边双连通分量缩成一个点,保留不同边双连通分量之间的边,最后会形成一颗树/森林。(因为对于无向图,去掉重边&自环之后,如果边数大于点数-1,那么必定会形成一个环,而在环上的所有点边双连通,形成一个大的边双连通分量,所以最后一定是一颗树/森林)。

图内两个之间的割边,就是两个点所在的边双连通分量代表的点,在缩完点之后形成的那颗树,两点相连经过的边

(2)点双连通

两个点双最多只有一个公共点,且一定是割点。(根据定义,如果两个点双之间有两个公共点,那么分属两个点双的两个点就至少有一条路径分别经过这两个公共点相连,会形成一个大点双)。

若两点双有交,那么交点一定是割点。(由上面的那个性质推出)

由于点双之间具有公共点,所以点双不具有传递性

一个点是割点当且仅当它属于超过一个点双,由一条边直接相连的两点点双连通。(根据后半句可以推出一条边恰属于一个点双,刚好同割边相反)。

对于一个点双,它在 DFS 搜索树中 dfn 值最小的点一定是割点或者树根。对于这个性质,分类讨论:

  • 当这个点为割点时,它一定是点双连通分量的根,因为一旦包含它的父节点,他仍然是割点。

  • 当这个点为树根时:

    • 有两个及以上子树,它是一个割点。

    • 只有一个子树,它是一个点双连通分量的根。

    • 它没有子树,视作一个点双。

需要注意:两点之间任意一条路径上的所有割点,不一定就是两点之间的所有必经点。

3.求法:

(1)边双连通分量

由于已经知道如何求割边了,那么割边分割出来的一块块连通块就是一个个边双连通分量。类似求强连通分量,在 dfs 的时候用一个存下当前遍历过的点,如果判断出 uv 是一条割边,那么 v 子树内的点(且还没有属于其他边双)就属于一个边双,弹出栈内元素直到弹到 v 。最后由于没有到达根的边,自然也就没有割边,最后需要特殊的将所有还在栈内的元素弹出(因为可能不止根节点一个点),它们属于同一个边双。

模板题 代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack>
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=2e6+5;
int n,m,num;
int low[M],dfn[M];
vector<vector<int>> ans;
stack<int> s;

int cnt=0;
struct N{
	int to,next,val;
}; N p[M<<1];
int head[M];
inline void add(int a,int b,int c)
{
	++cnt;
	p[cnt].next=head[a];
	head[a]=cnt;
	p[cnt].to=b,p[cnt].val=c;
}
inline void push(int pos)//表示直到弹到pos才结束
{
	vector<int> res;int x;
	while(x!=pos)
	{
		res.push_back((x=s.top()));
		s.pop();
	}
	ans.push_back(res);
}

inline void tarjan(int u,int pre)
{
	dfn[u]=low[u]=++num;
	s.push(u);
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(p[i].val==pre) continue;
		if(!dfn[v])
		{
			tarjan(v,p[i].val);
			low[u]=min(low[u],low[v]);
			if(low[v]>dfn[u]) push(v);//是割边
		}
		else low[u]=min(low[u],dfn[v]);
	}
}//实质上是求割边的同时将原图分成了一个个连通块
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1,a,b;i<=m;i++)
	{
		cin>>a>>b;
		add(a,b,i),add(b,a,i);
	}
	for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,0),push(i);//将剩下的点弹出
	cout<<ans.size()<<"\n";
	for(auto it:ans)
	{
		cout<<it.size()<<" ";
		for(auto x:it) cout<<x<<" ";
		cout<<"\n";
	}
	return 0;
}

(2)点双连通分量

类似边双连通分量的求法,但是略有不同。如果一个点 u 存在一个儿子 v 满足了 dfnulowv ,那么 u 就是一个割点,这时存在一个点双连通分量含有 uv 子树内还没有被分配到其他点双的点,弹出只弹出到 v,但是要u 也塞进这个点双里,u 由于是割点可能还属于其它的点双。

这时候不需要向求割点那样特判根节点,但是要特判单独的一个点

模板题代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack>
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=5e5+5,N=2e6+5;
int n,m,root,num;
int dfn[M],low[M];
stack<int> s;
vector<vector<int>> ans;
vector<int> res;

int cnt=0;
struct edge{
	int to,next;
};edge p[N<<1];
int head[M];
inline void add(int a,int b)
{
	++cnt;
	p[cnt].next=head[a];
	head[a]=cnt;
	p[cnt].to=b;
}
inline void push(int u,int f)//弹到 u,但是 f 点也要加进去
{
	res.clear();int x;
	while(1)
	{
		res.push_back((x=s.top()));
		s.pop();
		if(x==u) break;
	}
	res.push_back(f);
	ans.push_back(res);
}
inline void dfs(int u,int f)
{
	dfn[u]=low[u]=++num;
	s.push(u);
	int siz=0;
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(v==f) continue;
		if(!dfn[v])
		{
			++siz;
			dfs(v,u);
			low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u]) push(v,u);//通过 u -> v 判断出 u 是个割点
		}
		else low[u]=min(low[u],dfn[v]);
	}
	if(!f&&!siz)//特判单点
	{
		res.clear();
		res.push_back(u),ans.push_back(res);
	}
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1,a,b;i<=m;++i) cin>>a>>b,add(a,b),add(b,a);
	
	for(int i=1;i<=n;++i)
	{
		if(!dfn[i])
		{
			while(!s.empty()) s.pop();
			root=i,dfs(i,0);//root其实没啥用
		}
	}
	cout<<ans.size()<<"\n";
	for(auto res:ans)
	{
		cout<<res.size()<<" ";
		for(int it:res) cout<<it<<" ";
		cout<<"\n";
	}
	return 0;
}

4.习题:

CF1986F Non-academic Problem

给定一张无向图,可以删去任意一条边,询问最后图内可达性点对最少能为多少。

判断一道题需要使用点双还是边双求解,就观察题目询问的/进行操作的和什么相关。这道题明显就是和边相关,考虑使用边双。

对于一张无向图,如果删去一条非割边,并不会影响任一点的可达性,原图可达性点对不变,自然不优。由于要求最小,所以一定删去一条割边较优,并且这条割边减少的可达性点对最多。对于一条割边,删去这条割边,那么其连接的两边就会形成两个连通块 G1,G2,那么减少的可达性点对数量就是两边所含点数的乘积 sizG1sizG2

对于原图缩点,记录一下每个边双包含的点数,保留不同边双的边,形成一个森林。此时对于每一棵树,统计一下其包含的点数,记作 totsiz,并且它的边都是割边,那么对于边 uv 能减少的点对数量就是 sizv(totsizsizv),记录最大值。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack>
#define int long long
#define pii pair<int,int>
#define mk make_pair
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=2e5+5;
int T,n,m,num,tot,totsiz,ans,res;
int dfn[M],low[M],col[M],siz[M];
bool vis[M];
stack<int> s;
vector<int> e[M];

int cnt=0;
struct N{
	int to,next,val;
}; N p[M<<1];
int head[M];
inline void add(int a,int b,int c)
{
	++cnt;
	p[cnt].next=head[a];
	head[a]=cnt;
	p[cnt].to=b,p[cnt].val=c;
}
inline void push(int u)
{
	++tot;int x;
	while(1)
	{
		col[(x=s.top())]=tot,++siz[tot];//给每个点染色&记录边双大小
		s.pop();
		if(x==u) break;
	}
}
inline void dfs(int u,int pre)
{
	dfn[u]=low[u]=++num;
	s.push(u);
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(p[i].val==pre) continue;
		if(!dfn[v])
		{
			dfs(v,p[i].val);
			low[u]=min(low[u],low[v]);
			if(low[v]>dfn[u]) push(v);
		}
		else low[u]=min(low[u],dfn[v]);
	}
}
inline void dfs2(int u,int f)
{
	vis[u]=1;
	for(int v:e[u])
	{
		if(v==f) continue;
		dfs2(v,u);
		siz[u]+=siz[v];
	}
}
inline void dfs3(int u,int f)
{
	for(int v:e[u])
	{
		if(v==f) continue;
		ans=max(ans,siz[v]*(totsiz-siz[v]));//当前这条边做的贡献
		dfs3(v,u);
	}
}
inline void clear()
{
	cnt=num=tot=ans=res=0;
	for(int i=1;i<=n;i++)
	{
		head[i]=dfn[i]=low[i]=col[i]=siz[i]=vis[i]=0;
		e[i].clear();
	}
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>T;
	while(T--)
	{
		cin>>n>>m,clear();
		for(int i=1,a,b;i<=m;i++) cin>>a>>b,add(a,b,i),add(b,a,i);
		for(int i=1;i<=n;i++)
		{
			if(!dfn[i]) dfs(i,0),push(i);//点双缩点
		}
		for(int u=1;u<=n;u++)
		{
			for(int i=head[u];i!=0;i=p[i].next)
			{
				int v=p[i].to;
				if(col[u]==col[v]||v<=u) continue;
				e[col[u]].push_back(col[v]),e[col[v]].push_back(col[u]);//对于不同的边双保留边(有时候重边可能会影响答案,需要用个map判下重边)
			}
		}
		for(int i=1;i<=tot;i++)
		{
			if(!vis[i])
			{
				dfs2(i,0);//由于原图可能不连通,每一颗树单独处理出totsiz
				totsiz=siz[i],res+=totsiz*(totsiz-1)/2;//对于当前这棵树的任意两个点肯定都是连通的
				dfs3(i,0);
			}
		}
		cout<<res-ans<<"\n";
	}
	return 0;
}

CF1000E We Need More Bosses

对于一张无向连通图,找到两个点 s,t,使得 st 必须经过的边最多,求出这个最大值。

还是与边相关,考虑边双。必须经过的边不就是割边,考虑缩点后建出边双树(姑且这么叫吧),那么这颗树上的边就是必须经过的边。要找树上一条最长的路径,不就是树的直径(挺简单的,不会去搜搜看,网上挺多讲解的)。于是答案就是边双缩点之后形成的树的直径。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack>
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=3e5+5;
int n,m,num,tot,root,maxx;
int dfn[M],low[M],col[M];
stack<int> s;
vector<int> e[M];

int cnt=0;
struct N{
	int to,next,val;
}; N p[M<<1];
int head[M],deep[M];
inline void add(int a,int b,int c)
{
	++cnt;
	p[cnt].next=head[a];
	head[a]=cnt;
	p[cnt].to=b,p[cnt].val=c;
}
inline void push(int pos)
{
	int x;++tot;
	while(1)
	{
		col[(x=s.top())]=tot;
		s.pop();
		if(x==pos) break;
	}
}
inline void dfs(int u,int pre)
{
	dfn[u]=low[u]=++num;
	s.push(u);
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(p[i].val==pre) continue; 
		if(!dfn[v])
		{
			dfs(v,p[i].val);
			low[u]=min(low[u],low[v]);
			if(low[v]>dfn[u]) push(v);
		}
		else low[u]=min(low[u],dfn[v]);
	}
}

inline void dfs2(int u,int f,int d)
{
	deep[u]=d;
	for(auto v:e[u])
	{
		if(v==f) continue;
		dfs2(v,u,d+1);
	}
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1,a,b;i<=m;i++) cin>>a>>b,add(a,b,i),add(b,a,i);
	dfs(1,0),push(1);//保证了是连通图
	for(int u=1;u<=n;u++)
	{
		for(int i=head[u];i!=0;i=p[i].next)
		{
			int v=p[i].to;
			if(col[u]==col[v]||v>=u) continue;
			e[col[u]].push_back(col[v]),e[col[v]].push_back(col[u]);
		}
	}
	//以下都是在找直径
	dfs2(1,0,0);
	root=1,maxx=0;
	for(int i=2;i<=n;i++)
	{
		if(deep[i]>maxx) maxx=deep[i],root=i;
	}
	dfs2(root,0,0),maxx=0;
	for(int i=1;i<=n;i++) maxx=max(maxx,deep[i]);
	cout<<maxx<<"\n";
	return 0;
}

P2860 [USACO06JAN] Redundant Paths G

神秘推性质题,简化题意:给定一张无向图,问最少添加多少条边可以使得原图变成一个边双连通图

首先都问你边双了,那自然是考虑边双求解。缩点建树后,思考对于树上不直接连通的两个点 u,v,如果加入一条 uv 的无向边会有什么影响?u,v 都有两条到达对方的路径,一种是经过原树边,一种是通过新加的这条边。拓展一下,我们发现树上 uv 路径上的所有点都有两种到达彼此的方法,于是 uv 路径上的所有点成为了一个大的边双(化成了一个点)。

问题问的就是使得树化为一个点需要的最少操作。考虑下面的这颗树,怎么操作最优?肯定是从叶子出发到达另外一个叶子时最优,这个时候能够消除最多的点化作一个点(58),还有一个原因是每个叶子必须都要被操作一次,因为不可能有非叶子的两个节点之间的路径经过叶子。那么我们一直选择两个叶子,就一定最优了(5873 就消完了)。

最后答案就是(叶子数量+1)/2,因为向上取整。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack>
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e4+5;
int n,m,num,tot,ans;
int dfn[M],low[M],col[M],in[M];
stack<int> s;

int cnt=0;
struct N{
	int to,next,val;
}; N p[M<<1];
int head[M];
inline void add(int a,int b,int c)
{
	++cnt;
	p[cnt].next=head[a];
	head[a]=cnt;
	p[cnt].to=b,p[cnt].val=c;
}
inline void push(int pos)
{
	int x;++tot;
	while(1)
	{
		col[(x=s.top())]=tot;
		s.pop();
		if(x==pos) break;
	}
}
inline void dfs(int u,int pre)
{
	dfn[u]=low[u]=++num,s.push(u);
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(p[i].val==pre) continue;
		if(!dfn[v])
		{
			dfs(v,p[i].val);
			low[u]=min(low[u],low[v]);
			if(low[v]>dfn[u]) push(v);
		}
		else low[u]=min(low[u],dfn[v]);
	}
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1,a,b;i<=m;++i)
	{
		cin>>a>>b;
		add(a,b,i),add(b,a,i);
	}
	dfs(1,0),push(1);//保证连通,只用做一遍边双缩点
	for(int u=1;u<=n;++u)
	{
		for(int i=head[u];i!=0;i=p[i].next)
		{
			int v=p[i].to;
			if(v<u&&col[v]!=col[u]) ++in[col[u]],++in[col[v]];
		}
	}
	for(int i=1;i<=tot;++i) ans+=(in[i]==1);//叶子就是度数为1的点嘛
	cout<<(ans+1)/2<<"\n";
	return 0;
}

P3469 [POI2008] BLO-Blockade

和 CF1986F Non-academic Problem 很像啊,只不过那道题删边,而这道题是删点。与点相关,考虑使用点双&割点的性质求解。

对于每个点 i,考虑删去与点 i 相连的所有边(实际就是把 i 删了嘛),图上有几个不可达性点对。

对于非割点,删去之后不会影响其他点的连通性,只是其他点到达不了自己,除开自己,其他点共有 n1 个,由于没有顺序,所以答案是 2(ni)

对于割点,删去之后连通块数量增加(注意不一定只增加 1,如果这个割点在多个连通块的中间的话,类似菊花图,可能会多出相当多的连通块)。考虑一个割点 u,将其删去会多出一些连通块 G1,G2......Gn,对于一个连通块 G1 来说,设其大小为 sizG1,原本连通块内的点可以与外面 nsizG1 个点连通,而现在不行了,减少的点对数量就是 sizG1nsizG1,把每个连通块减少的数量相加,这些都可以在 dfs 的过程中统计出来。但是还有一个连通块在 u 的上面,这个连通块的大小自然是 nsum1,sum=sizG1+sizG2+......+sizGn,减去 1 是因为要减去 u 这个点。同时还有割去 un1 的贡献,至于为什么不是 2(n1),是因为在计算连通块的贡献的时候已经统计了一遍 u 对其中每个点形成点对的数量。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=5e5+5;
int n,m,num;
int dfn[M],low[M],siz[M],ans[M];
bool vis[M]; 

int cnt=0;
struct N{
	int to,next;
}; N p[M<<1];
int head[M];
inline void add(int a,int b)
{
	++cnt;
	p[cnt].next=head[a];
	head[a]=cnt;
	p[cnt].to=b;
}
inline void dfs(int u,int f)
{
	dfn[u]=low[u]=++num,siz[u]=1;
	int son=0,sum=0;
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(v==f) continue;
		if(!dfn[v])
		{
			++son,dfs(v,u);
			low[u]=min(low[u],low[v]),siz[u]+=siz[v];
			if(dfn[u]<=low[v])
			{
				ans[u]+=siz[v]*(n-siz[v]),sum+=siz[v];
				if(u!=1)  vis[u]=1;
				//如果当前点删去之后 v 子树就成一个连通块了,就要加上贡献,同时统计 sum
			}
		}
		else low[u]=min(low[u],dfn[v]);
	}
	if(son>1&&u==1) vis[u]=1;
	if(!vis[u]) ans[u]=2*(n-1);//不是割点答案就是 2*(n-1)
	else ans[u]+=(n-sum-1)*(sum+1)+(n-1);//是的话还要加上剩下的那一个连通块与 u 自身的贡献
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	//freopen("P3469_1.in","r",stdin);
	cin>>n>>m;
	for(int i=1,a,b;i<=m;++i)
	{
		cin>>a>>b;
		add(a,b),add(b,a);
	}
	dfs(1,0);
	for(int i=1;i<=n;i++) cout<<ans[i]<<"\n";
	return 0;
}

P5058 [ZJOI2004] 嗅探器

考察了 dfn 数组的一些性质。对于一张无向图,给出两个点 a,b,询问两个点之间所有必经点中编号最小的点。

乍一看去似乎不是很可做,因为点双和边双性质上有不同,两点之间任意一条路径上的割点,不一定是两点之间的必经点(如下图中 13 的路径中有一条是 145,4是割点但不是必经点)。

那么为了判断某个点是两点之间的必经点,现在唯一已知的是这个点肯定是个割点。那么我们就枚举每一个割点,看删去这个割点之后两点是否连通,不连通那肯定就是必经点了。为了方便判断两点是个否连通,可以利用一下 dfn 数组的性质。

首先以两点中的一点为根 ,这里以 a 为根,那么 Tarjan 判割点的同时,找到一个割点 u,由 uv 这条边判断出来,观察 dfnbdfnv 是否存在 dfnvdfnb 的关系,如果存在,那么这个割点 u 就是一个必经点。

这样做的依据?由于在 dfs 生成树中,子树内的点的 dfn 值一定比根的 dfn 值大。对于由边 uv 判断出来的割点 u,说明存在 dfnulowv,也就是说删去 u 后以 v 为根的子树就不与外界连通了,而如果 b 还在 v 子树内的话,那么自然 a 就不与 b 连通了。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=5e5+5,inf=2e9;
int n,a,b,num,ans=inf;
int dfn[M],low[M];

int cnt=0;
struct N{
	int to,next;
}; N p[M<<1];
int head[M];
inline void add(int a,int b)
{
	++cnt;
	p[cnt].next=head[a];
	head[a]=cnt;
	p[cnt].to=b;
}
inline void dfs(int u,int f)
{
	dfn[u]=low[u]=++num;
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(v==f) continue;
		if(!dfn[v])
		{
			dfs(v,u);
			low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u])
			{
				//u是一个割点
				if(u!=a&&dfn[b]>=dfn[v]) ans=min(ans,u);//b在v子树内,u删去之后a,b必不连通,则u是必经点
			}
		}
		else low[u]=min(low[u],dfn[v]);
	}
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	while(1)
	{
		cin>>a>>b;
		if(!a&&!b) break;
		add(a,b),add(b,a);
	}
	cin>>a>>b;
	dfs(a,0);
	if(ans==inf) cout<<"No solution\n";
	else cout<<ans<<"\n";
	return 0;
}

P3225 [HNOI2012] 矿场搭建

非常妙的一道题。对于一张无向图,可以指定一些点为逃生点,删去任意一个点,都可以保证剩下的点可以与至少一个逃生点相连通,询问至少需要指定多少个逃生点,以及指定最少逃生点的方案数。

由于是和点相关,考虑点双&割点相关的性质。由于需要删去一个点,那么我们先考虑朴素情况,如果原图是一张点双连通图,那么删哪个点都没有影响,那么我们只需要随意指定两个点作为逃生点(只指定一个的话,可能删的就刚好是逃生点,然后剩下的点就没法和逃生点相连了),方案数显然为 n(n1)/2

那么对于原图中存在割点的时候,怎么分配逃生点才能使得分配的数量最少?考虑原图是如下一张图,存在 4 个点双 1,2,3,4,5,6,7,8,9,3,6,9,10 ,但通过手动模拟我们发现实际上只需要在前三个点双,每个点双选择一个非割点的点作为逃生点即可,例如放1,4,7 怎么卡都卡不掉。

实际上,对于这些只存在一个割点的点双实际上就类似于树上的叶子,将这些叶子做上标记,那么随便断一个点,每个点还是能保证到达一个叶子节点(对于链这种极端情况,链的两端都会被看作叶子节点)。

于是答案是这些被看作叶子节点的节点数,也就是只含有一个割点的点双,方案数也比较简单,由于每一个叶子点双子需要任选一个不是割点的点作为逃生点,所以答案就是 Πsizi1i 只含有一个割点的点双。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e3+5;
int n,m,a,b,num,tot,root,ans,sum;
int dfn[M],low[M];
bool vis[M];
stack<int> s;
vector<int> t[M]; 

int cnt=0;
struct N{
	int to,next;
}; N p[M<<1];
int head[M];
inline void add(int a,int b)
{
	++cnt,n=max(n,max(a,b));
	p[cnt].next=head[a];
	head[a]=cnt;
	p[cnt].to=b;
}
inline void push(int u,int f)
{
	++tot;int x;
	while(1)
	{
		t[tot].push_back((x=s.top()));//统计每个点双内的点
		s.pop();
		if(x==u) break;
	}
	t[tot].push_back(f);
}
inline void dfs(int u,int f)
{
	dfn[u]=low[u]=++num;
	s.push(u);
	int siz=0;
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(v==f) continue;
		if(!dfn[v])
		{
			++siz;
			dfs(v,u);
			low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u])
			{
				push(v,u);
				if(u!=root) vis[u]=1;
			}
		}
		else low[u]=min(low[u],dfn[v]);
	}
	if(u==root&&siz>1) vis[u]=1;
}
inline void clear()
{
	cnt=n=num=tot=ans=0,sum=1;
	memset(dfn,0,sizeof(dfn)),memset(low,0,sizeof(low));
	memset(head,0,sizeof(head)),memset(vis,0,sizeof(vis));
	for(int i=1;i<M;++i) t[i].clear();
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	for(int T=1;T<=10000;++T)
	{
		cin>>m,clear(); 
		if(!m) break;
		for(int i=1;i<=m;++i) cin>>a>>b,add(a,b),add(b,a);
		for(int i=1;i<=n;++i)
		{
			if(!dfn[i])
			{
				while(!s.empty()) s.pop();
				root=i,dfs(i,0);
			}//点双
		}
		for(int i=1,res;i<=tot;++i)
		{
			res=0;
			for(int it:t[i]) res+=vis[it];//看当前点双有几个割点
			if(res==1) ++ans,sum*=(t[i].size()-1);//如果只有一个割点就统计答案
		}
		if(tot==1)
		{
			cout<<"Case "<<T<<": "<<2<<" "<<n*(n-1)/2<<"\n";
		}
		else cout<<"Case "<<T<<": "<<ans<<" "<<sum<<"\n";
	}
	return 0;
}

AT_abc334_g [ABC334G] Christmas Color Grid 2

均匀随机一个绿色块换成红色,求绿色四连通块的期望数量。题意非常清新啊,那么我们就可以枚举每个位置绿颜色块转化成红色之后形成的连通块数量,用每个位置变化后得到的绿色四连通块个数和除去绿色颜色个数就是期望答案了。

那么如何快速判定一个绿点转化红点之后连通块的数量,和点相关,所以考虑点双,那么为了跑点双,肯定需要把图建出来,那么每个绿色块就像自己的上下左右的绿色块连边。于是我们就可以用点双的性质来解题了,首先对于一个孤独的单点,删去之后自然连通块数量 -1,对于一个非割点,删去之后不影响连通性,所以连通块的数量不变。着重考虑的是割点,对于一个同时在 x 个点双内的割点,如果这个割点删去之后,那么这 x 个点双就互相两两不相通了,于是增加的连通块就是 x1

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=2e3+5,N=4e6+5,mod=998244353;
int n,m,num,res,sum,root;
char opt;
int a[M][M],dfn[N],low[N],c[N];
bool vis[N];
stack<int> s;

int cnt=0;
struct edge{
	int to,next;
};edge p[N<<1];
int head[N];
inline int pos(int i,int j){return (i-1)*m+j;}
inline void add(int a,int b)
{
	++cnt;
	p[cnt].next=head[a];
	head[a]=cnt;
	p[cnt].to=b;
}
inline int quick(int a,int n)
{
	int res=1;
	while(n)
	{
		if(n&1) res=res*a%mod;
		n>>=1,a=a*a%mod; 
	}
	return res;
}
inline int inv(int x) {return quick(x,mod-2);}
inline void push(int u,int f)
{
	int x;
	while(1)
	{
		++c[(x=s.top())];
		s.pop();
		if(u==x) break;
	}
	++c[f];
}
inline void dfs(int u,int f)
{
	dfn[u]=low[u]=++num;
	s.push(u);int siz=0;
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(v==f) continue;
		if(!dfn[v])
		{
			++siz,dfs(v,u);
			low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u]) vis[u]=1,push(v,u);
		}
		else low[u]=min(low[u],dfn[v]);
	}
	if(!f&&!siz) vis[u]=1;//孤独单点
}//点双
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	//freopen("2.in","r",stdin);
	cin>>n>>m;
	for(int i=1;i<=n;++i)
	{
		for(int j=1;j<=m;++j) cin>>opt,a[i][j]=(opt=='#');
	}
	for(int i=1;i<=n;++i)
	{
		for(int j=1;j<=m;++j)
		{
			if(!a[i][j]) continue;
			++sum;
			if(a[i+1][j]) add(pos(i,j),pos(i+1,j)),add(pos(i+1,j),pos(i,j));
			if(a[i][j+1]) add(pos(i,j),pos(i,j+1)),add(pos(i,j+1),pos(i,j));//先连边,每个绿点只用向自己的右下方连边即可
		}
	}
	for(int i=1;i<=n;++i)
	{
		for(int j=1;j<=m;++j)
		{
			if(!a[i][j]||dfn[pos(i,j)]) continue;
			while(!s.empty()) s.pop();
			++res,root=pos(i,j),dfs(pos(i,j),0);
		}
	}
	res*=sum;
	for(int i=1;i<=n*m;++i)
	{
		if(vis[i]) res+=c[i]-1;//每一个割点,就是其所在的点双个数-1,把孤独一个单点 vis 也记成1,那么刚好贡献也为 -1
	}
	cout<<res%mod*inv(sum)%mod<<"\n";//费马小定理求逆元
	return 0;
}

P2783 有机化学之神偶尔会做作弊

给定一张无向图,所有的环缩成一个点,每次询问两个点之间有多少个点。一张无向图,不存在环,那不就是一颗树了,但是题目没有保证图连通,所以处理之后的图应该是一个森林。

处理环,一般使用边双处理,那么边双缩完点之后的树就是题目要求的操作,那么我们就在这些树上进行询问。求任意两个点之间的点个数,实际上就是求树上两点的路径长度+1,有 n 种方法去做,我用的是树剖(因为顺手),x,y 两点的距离就是 deepx+deepy2deeplcax,y

注意:由于要求两个点不成环,所以两点之间不能存在重边,需要用 map 判下重。输出也有点鬼畜。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<map>
#include<stack>
#define pii pair<int,int>
#define mk make_pair
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=5e4+5;
int n,m,q,num,tot;
int dfn[M],low[M],col[M],pre[25];
stack<int> s;
map<pii,int> mapp;
vector<int> e[M]; 

int cnt=0;
struct N{
	int to,next,val;
}; N p[M<<1];
int head[M];
inline void add(int a,int b,int c)
{
	++cnt;
	p[cnt].next=head[a];
	head[a]=cnt;
	p[cnt].to=b,p[cnt].val=c;
}
inline void push(int u)
{
	int x;++tot;
	while(1)
	{
		col[(x=s.top())]=tot;
		s.pop();
		if(x==u) break;
	}
}//处理边双
inline void dfs(int u,int pre)
{
	dfn[u]=low[u]=++num;
	s.push(u);
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(p[i].val==pre) continue;
		if(!dfn[v])
		{
			dfs(v,p[i].val);
			low[u]=min(low[u],low[v]);
			if(low[v]>dfn[u]) push(v);
		}
		else low[u]=min(low[u],dfn[v]);
	}
}
int deep[M],siz[M],fa[M],son[M],top[M];
inline void dfs1(int u,int f,int d)
{
	deep[u]=d,siz[u]=1,fa[u]=f;
	for(int v:e[u])
	{
		if(v==f) continue;
		dfs1(v,u,d+1);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]]) son[u]=v;
	}
}
inline void dfs2(int u,int topp)
{
	top[u]=topp;
	if(!son[u]) return ;
	dfs2(son[u],topp);
	for(int v:e[u])
	{
		if(!top[v]) dfs2(v,v);
	}
}
inline int LCA(int x,int y)
{
	while(top[x]!=top[y])
	{
		if(deep[top[x]]<deep[top[y]]) swap(x,y);
		x=fa[top[x]];
	}
	if(deep[x]>deep[y]) return y;
	return x;
}
inline int length(int x,int y){return deep[x]+deep[y]-2*deep[LCA(x,y)];}
inline void print(int x)
{
	bool flag=0;
	for(int i=19;i>=0;--i)
	{
		if(x>=pre[i]) flag|=1,cout<<1,x-=pre[i];
		else if(flag) cout<<0;
	}
	cout<<"\n";
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m,pre[0]=1;
	for(int i=1;i<=19;++i) pre[i]=pre[i-1]*2;
	for(int i=1,a,b;i<=m;++i)
	{
		cin>>a>>b;
		if(mapp[mk(a,b)]) continue;//判重边
		mapp[mk(a,b)]=mapp[mk(b,a)]=1;
		add(a,b,i),add(b,a,i);
	}
	mapp.clear();
	for(int i=1;i<=n;++i)
	{
		if(!dfn[i]) dfs(i,0),push(i);//边双处理
	}
	for(int u=1;u<=n;++u)
	{
		for(int i=head[u];i!=0;i=p[i].next)
		{
			int v=p[i].to;
			if(col[u]==col[v]||mapp[mk(col[u],col[v])]) continue;//这里也别有重边了,因为有siz数组,有重边siz数组会算多次
			e[col[u]].push_back(col[v]),e[col[v]].push_back(col[u]);
			mapp[mk(col[u],col[v])]=mapp[mk(col[v],col[u])]=1;
		}
	}
	dfs1(1,0,1),dfs2(1,1);//经典树剖
	cin>>q;int x,y,len;
	while(q--)
	{
		cin>>x>>y,x=col[x],y=col[y],len=length(x,y)+1;//答案是路径+1
		print(len);
	}
	return 0;
}

P3854 [TJOI2008] 通讯网破坏

实际上是一道圆方树板子题,但是我们就按着点双的思路来。对于一张无向图,多次询问 s,t,m 表示 m 是否为 st 路径上的必经点。

于是我们就需要建出一种特殊的树来处理点双之间的连通性,这就是圆方树,这里简单叙述一下。有两种建发(具体我不是很清楚,有一种是所谓的广义圆方树)。

1.对于每一个割点新建一个点,所有点双化为一个点,每个割点建出来的新点向其所在的点双化作的点连一条边。

2.对于每一个点双连一条边,点双内的所有点向这个新建点连一条边,同时每个点双内原有的边都废弃。(广义)

建出来的圆方树,两点之间路径上的所有点才能被称之为必经点。于是树剖之后单点修改 m,然后判断 st 路径上有没有值就完事了。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack>
#include<map>
#define pii pair<int,int>
#define mk make_pair
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e5+5;
int n,m,q,num,tot,edge_num;
int dfn[M],low[M],col[M];
bool vis[M];
stack<int> s;
map<pii,int> mapp;
vector<int> e[M],res;
vector<vector<int>> ans;

int cnt=0;
struct N{
	int to,next;
}; N p[M<<1];
int head[M];
inline void add(int a,int b)
{
	++cnt;
	p[cnt].next=head[a];
	head[a]=cnt;
	p[cnt].to=b;
}
inline void push(int u,int f)
{
	++tot,res.clear();int x;
	while(1)
	{
		res.push_back((x=s.top()));
		s.pop();
		if(x==u) break;
	}
	res.push_back(f);
	ans.push_back(res);
}
inline void dfs(int u,int f)
{
	dfn[u]=low[u]=++num;
	s.push(u);
	int siz=0;
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(v==f) continue;
		if(!dfn[v])
		{
			++siz;
			dfs(v,u);
			low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u]) push(v,u),vis[u]=1;
		}
		else low[u]=min(low[u],dfn[v]);
	}
}
int deep[M],siz[M],fa[M],son[M],top[M],id[M];
inline void dfs1(int u,int f,int d)
{
	deep[u]=d,siz[u]=1,fa[u]=f;
	for(int v:e[u])
	{
		if(v==f) continue;
		dfs1(v,u,d+1);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]]) son[u]=v;
	}
}
inline void dfs2(int u,int topp)
{
	top[u]=topp,id[u]=++num;
	if(!son[u]) return ;
	dfs2(son[u],topp);
	for(int v:e[u]) if(!top[v]) dfs2(v,v);
}
struct SGT{
	int tree[M<<2];
	inline void update(int u,int ll,int rr,int x,int k)
	{
		if(ll==rr) {tree[u]+=k;return ;}
		int mid=(ll+rr)>>1;
		if(mid>=x) update(u<<1,ll,mid,x,k);
		else update(u<<1|1,mid+1,rr,x,k);
		tree[u]=tree[u<<1]+tree[u<<1|1];
	}
	inline int query(int u,int ll,int rr,int L,int R)
	{
		if(L<=ll&&rr<=R) return tree[u];
		int mid=(ll+rr)>>1,res=0;
		if(mid>=L) res+=query(u<<1,ll,mid,L,R);
		if(R>mid) res+=query(u<<1|1,mid+1,rr,L,R);
		return res;
	}
	inline int ask(int x,int y)
	{
		int ans=0;
		while(top[x]!=top[y])
		{
			if(deep[top[x]]<deep[top[y]]) swap(x,y);
			ans+=query(1,1,tot,id[top[x]],id[x]);
			x=fa[top[x]];
		}
		if(deep[x]>deep[y]) swap(x,y);
		ans+=query(1,1,tot,id[x],id[y]);
		return ans;
	}
};SGT t;//树剖+线段树

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1,a,b;i<=m;i++) cin>>a>>b,add(a,b),add(b,a);
	dfs(1,0);
	for(int i=1;i<=n;i++) if(vis[i]) col[i]=++tot;//对于每一个割点新建一个点
	int pos=0;
	for(auto res:ans)
	{
		++pos;
		for(int it:res)//建边1所说的
		{
			if(vis[it])
			{
				e[col[it]].push_back(pos),e[pos].push_back(col[it]);
			}
			else col[it]=pos;
		}
	}
	num=0;
	dfs1(1,0,1),dfs2(1,1);//数据结构部分了
	cin>>q;int k,x,y;
	while(q--)
	{
		cin>>x>>y>>k;
		if(!vis[k]) {cout<<"no\n";continue;}
		x=col[x],y=col[y],k=col[k];
		if(x==k||y==k) {cout<<"no\n";continue;}
		t.update(1,1,tot,id[k],1);
		if(t.ask(x,y)) cout<<"yes\n";
		else cout<<"no\n";
		t.update(1,1,tot,id[k],-1);
	}
	return 0;
}

P7687 [CEOI2005] Critical Network Lines

一张无向图连通上,有 A,B 两种类型的点若干个, 每一个点可能同时是两种类型的点。询问割去那些边会使得存在点所在的连通块不存在 A 类型的点或 B 类型的点。

询问边,考虑采用边双求解,由于边双缩点建树后树的性质较为优美,那么我们就建树,在缩点的过程中统计一下这个边双含有的 A,B 类型的点的个数。由于图连通,最后会形成一棵树,由于要使得原图出现不连通的情况才有可能满足存在点所在的连通块不存在 A 类型的点或 B 类型的点,所以求的边只能是割边,也就是新建出来的树上的这些边。

一棵树,删去一条边,一定只会分成两个连通块。那么只要其中一个连通块内的 A,B 类型的点的数量为 0 了,那么这条边就满足要求了,那么先 dfs 一遍 统计出以每个点为子树时,子树内的 A,B 类型的点。再 dfs 一遍,对于边 uv ,如果以 v 为子树时,子树内的 A,B 类型的点为 0,或者说子树内包含了所有类型 AB 的点,那么就记录下来。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<stack>
#include<map>
#define pii pair<int,int>
#define mk make_pair
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e5+5,N=1e6+5;
int n,m,k,l,num,tot,tota,totb,res;
int dfn[M],low[M],siza[M],sizb[M],col[M];
bool visa[M],visb[M];
stack<int> s;
vector<int> e[M];
pii a[M];
map<pii,pii> mapp;

int cnt=0;
struct edge{
	int to,next,val;
};edge p[N<<1];
int head[M];
inline void add(int a,int b,int c)
{
	++cnt;
	p[cnt].next=head[a];
	head[a]=cnt;
	p[cnt].to=b,p[cnt].val=c;
}
inline void push(int u)
{
	++tot;int x;
	while(1)
	{
		col[(x=s.top())]=tot;
		siza[tot]+=visa[x],sizb[tot]+=visb[x];
		s.pop();
		if(x==u) break;
	}
}
inline void dfs(int u,int pre)
{
	dfn[u]=low[u]=++num;
	s.push(u);
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(p[i].val==pre) continue;
		if(!dfn[v])
		{
			dfs(v,p[i].val);
			low[u]=min(low[u],low[v]);
			if(low[v]>dfn[u]) push(v);
		}
		else low[u]=min(low[u],dfn[v]);
	}
}
inline void dfs1(int u,int f)
{
	for(int v:e[u])
	{
		if(v==f) continue;
		dfs1(v,u);
		siza[u]+=siza[v],sizb[u]+=sizb[v];
	}
}
inline void dfs2(int u,int f)
{
	for(int v:e[u])
	{
		if(v==f) continue;
		//如果v子树内没有A类型的点 或 v子树内没有B类型的点 或 v子树包含所有A类型的点 或 v子树包含所有B类型的点
		if(!siza[v]||!sizb[v]||!(tota-siza[v])||!(totb-sizb[v])) a[++res]=mapp[mk(u,v)];
		dfs2(v,u);
	}
}
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m>>k>>l;
	for(int i=1,x;i<=k;++i) cin>>x,visa[x]=1;
	for(int i=1,x;i<=l;++i) cin>>x,visb[x]=1;
	for(int i=1,a,b;i<=m;++i) cin>>a>>b,add(a,b,i),add(b,a,i);
	dfs(1,0),push(1);
	//边双缩点
	for(int u=1;u<=n;++u)
	{
		for(int i=head[u];i!=0;i=p[i].next)
		{
			int v=p[i].to;
			if(col[u]==col[v]||v<=u) continue;
			e[col[u]].push_back(col[v]),e[col[v]].push_back(col[u]);
			mapp[mk(col[u],col[v])]=mapp[mk(col[v],col[u])]=mk(u,v);
		}
	}//建树
	dfs1(1,0),tota=siza[1],totb=sizb[1];//第一遍预处理以每个点为根时子树的信息
	dfs2(1,0);
	cout<<res<<"\n";
	for(int i=1;i<=res;++i) cout<<a[i].first<<" "<<a[i].second<<"\n";
	return 0;
}

P7924 「EVOI-RD2」旅行家

卡常题,被卡惨了(所以不保证代码卡的过去,实在不行看一下其他解法)。在一张无向图上给定多组 s,t 起点终点,对于每一组起点终点走遍所有的路径,询问最后被遍历到的点的权值和。

由于要走所有的路径,所以考虑使用边双,使用边双缩点建树之后,记录一下每个边双含有点的权值和,对于一组 s,t,树上 s,t 所在的边双代表的点之间的所有边双都是可以在 st 路径上的,所以这些边双内的点都需要被统计答案。

问题就转化为对于一棵树,多次给定两点,将两点间路径上所有点+1,然后统计出所有权值不为 0 的点,这不树上差分板子题,对于 st++wx,++wy,wlcax,y,wfalcax,y,然后 dfs 做一遍类似统计子树大小的操作即可。

最后如果 wi!=0 ,就加上 i 这个边双的权值。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<map>
#include<stack>
#define pii pair<int,int>
#define mk make_pair
using namespace std;

namespace Fread
{
    const int SIZE=1<<21;
    char buf[SIZE],*S,*T;
    inline char getchar()
    {
        if (S==T)
        {
            T=(S=buf)+fread(buf,1,SIZE,stdin);
            if (S==T)
            {
                return '\n';
            }
        }
        return *S++;
    }
}
namespace Fwrite
{
    const int SIZE=1<<21;
    char buf[SIZE],*S=buf,*T=buf+SIZE;
    inline void flush()
    {
        fwrite(buf,1,S-buf,stdout);
        S=buf;
    }
    inline void putchar(char c)
    {
        *S++=c;
        if(S==T)
        {
            flush();
        }
    }
    struct NTR
    {
        ~NTR()
        {
            flush();
        }
    }ztr;
}
#ifdef ONLINE_JUDGE
#define getchar Fread :: getchar
#define putchar Fwrite :: putchar
#endif
namespace Fastio
{
    struct Reader
    {
        template<typename T>
        Reader&operator>>(T&x)
        {
            char c=getchar();
            T f=1;
            while(c<'0'||c>'9')
            {
                if (c=='-') f=-1;
                c=getchar();
            }
            x=0;
            while(c>='0'&&c<='9')
            {
                x=x*10+(c-'0');
                c=getchar();
            }
            x*=f;
            return *this;
        }
        Reader&operator>>(char&c)
        {
            c=getchar();
            while(c==' ' || c=='\n')
            {
                c=getchar();
            }
            return *this;
        }
        Reader&operator>>(char* str)
        {
            int len=0;
            char c=getchar();
            while (c==' '||c=='\n')
            {
                c=getchar();
            }
            while (c!=' '&&c!='\n'&&c!='\r')
            {
                str[len++]=c;
                c=getchar();
            }
            str[len]='\0';
            return *this;
        }
        Reader(){}
    }cin;
    const char endl='\n';
    struct Writer
    {
        template<typename T>
        Writer&operator<<(T x)
        {
            if(x==0)
            {
                putchar('0');
                return *this;
            }
            if(x<0)
            {
                putchar('-');
                x=-x;
            }
            static int sta[45];
            int top=0;
            while(x)
            {
                sta[++top]=x%10;
                x/=10;
            }
            while(top)
            {
                putchar(sta[top]+'0');
                --top;
            }
            return *this;
        }
        Writer&operator<<(char c)
        {
            putchar(c);
            return *this;
        }
        Writer&operator<<(char* str)
        {
            int cur=0;
            while(str[cur])
            {
                putchar(str[cur++]);
            }
            return *this;
        }
        Writer&operator<<(const char* str)
        {
            int cur=0;
            while(str[cur])
            {
                putchar(str[cur++]);
            }
            return *this;
        }
        Writer(){}
    }cout;
}
#define cin Fastio :: cin
#define cout Fastio :: cout
#define endl Fastio :: endl

inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
inline void swap(int &x,int &y){x^=y^=x^=y;}
const int M=5e5+5,N=2e6+5;
int n,m,q,num,tot,num_edge;
int c[M],dfn[M],low[M],col[M],w[M],sum[M];
map<pii,int> mapp;
stack<int> s;
pii e[M];

int cnt=0;
struct edge{
	int to,next,val;
};edge p[N<<1];
int head[M];
inline void add(int a,int b,int c=0)
{
	++cnt;
	p[cnt].next=head[a];
	head[a]=cnt;
	p[cnt].to=b,p[cnt].val=c;
}
inline void push(int u)
{
	++tot;int x;
	while(1)
	{
		col[(x=s.top())]=tot,sum[tot]+=c[x]; 
		s.pop();
		if(x==u) break;
	}
}
inline void dfs(int u,int pre)
{
	dfn[u]=low[u]=++num;
	s.push(u);
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(p[i].val==pre) continue;
		if(!dfn[v])
		{
			dfs(v,p[i].val);
			low[u]=min(low[u],low[v]);
			if(low[v]>dfn[u]) push(v);
		}
		else low[u]=min(low[u],dfn[v]);
	}
}
int deep[M],fa[M],siz[M],son[M],top[M];
inline void dfs1(int u,int f,int d)
{
	fa[u]=f,siz[u]=1,deep[u]=d;
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(v==f) continue;
		dfs1(v,u,d+1);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]]) son[u]=v;
	}
}
inline void dfs2(int u,int topp)
{
	top[u]=topp;
	if(!son[u]) return ;
	dfs2(son[u],topp);
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(!top[v]) dfs2(v,v);
	}
}
inline int LCA(int x,int y)
{
	while(top[x]!=top[y])
	{
		if(deep[top[x]]<deep[top[y]]) swap(x,y);
		x=fa[top[x]];
	}
	if(deep[x]>deep[y]) return y;
	return x;
}
inline void dfs3(int u,int f)
{
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(v==f) continue;
		dfs3(v,u);
		w[u]+=w[v];
	}
}
signed main()
{
	cin>>n>>m;
	for(int i=1;i<=n;++i) cin>>c[i];
	for(int i=1,a,b;i<=m;++i) cin>>a>>b,add(a,b,i),add(b,a,i);
	for(int i=1;i<=n;++i)
	{
		if(!dfn[i]) dfs(i,0),push(i);
	}
	for(int u=1;u<=n;++u)
	{
		for(int i=head[u];i!=0;i=p[i].next)
		{
			int v=p[i].to;
			if(col[u]==col[v]||mapp[mk(col[u],col[v])]) continue;
			e[++num_edge]=mk(col[u],col[v]);
			mapp[mk(col[u],col[v])]=mapp[mk(col[v],col[u])]=1;
		}
	}//边双缩点建树+去重边
	cnt=0;for(int i=1;i<=tot;++i) head[i]=0;//md,卡死了,链式前向星快一点
	for(int i=1;i<=num_edge;++i) add(e[i].first,e[i].second),add(e[i].second,e[i].first);
	dfs1(1,0,1),dfs2(1,1);
	cin>>q;int x,y,lca;
	while(q--)
	{
		cin>>x>>y,x=col[x],y=col[y],lca=LCA(x,y);
		++w[x],++w[y],--w[lca],--w[fa[lca]];//树上差分
	}
	dfs3(1,0);
	int ans=0;
	for(int i=1;i<=tot;++i) if(w[i]) ans+=sum[i];
	cout<<ans<<"\n";
	return 0;
}

P10875 [COTS 2022] 游戏 M

纯数据结构题,沾了一点边双的性质。

问题是有 n 个点, m条边,多次询问你至少需要添加 m 条边中的前多少条边才能使得 u,v 连通&不存在割边。触及割边,那肯定是与边双相关了。

首先考虑先连通,我们考虑就按照给定的边的顺序来,一条条加进去,用并查集维护一下,看当前边是否连通的是两个原本不连通的连通块,如果是,说明当前边肯定被当作树边,并查集 merge 一下,如果不是,就有可能被当成多余的边添加进图中,离线下来。

然后考虑这些多余的边有什么作用。因为询问时至少需要添加 m 条边中的前多少条边才能使得 u,v 连通&不存在割边,连通已经解决了,要使得 u,v 之间不存在割边,那么就说明 u,v 两点边双连通。

那么对于不是树边一条边 x,y ,在原图上添加上,那么 xy 路径上所有点都都化为一个边双。我们就可以初始化树的边权为 inf,然后边 xy 的权值就是它是出现的第几条边,记为 w。添加一条 xy 就是将 xy 的路径上的所有边的边权对 wmin,直至所有多余的都进行了处理。

那么每次询问两个点 u,v,首先先判断是否连通,不连通输出 1,连通的话查询 uv
上路径的最大值,如果最大值等于 inf,则说明加完边了这两点都还没有边双连通,输出 1,否则输出查询到的最大值。

使用树剖ing。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=3e5+5,inf=1e9;
int n,m,q,tot,root;
int F[M];

int cnt=0;
struct N{
	int to,next;
}; N p[M<<1];
struct node{
	int x,y,t;
};node a[M];
int head[M];
inline void add(int a,int b)
{
	++cnt;
	p[cnt].next=head[a];
	head[a]=cnt;
	p[cnt].to=b;
}
inline int find(int x)
{
	if(x!=F[x]) F[x]=find(F[x]);
	return F[x];
}
inline bool merge(int x,int y)
{
	x=find(x),y=find(y);
	if(x==y) return 0;
	F[x]=y;return 1;
}//并查集的操作

int deep[M],siz[M],fa[M],son[M],col[M];
int top[M],id[M],num;
inline void dfs1(int u,int f,int d)
{
	fa[u]=f,siz[u]=1,deep[u]=d,col[u]=root;
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(v==f) continue;
		dfs1(v,u,d+1);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]]) son[u]=v;
	}
}
inline void dfs2(int u,int topp)
{
	top[u]=topp,id[u]=++num;
	if(!son[u]) return ;
	dfs2(son[u],topp);
	for(int i=head[u];i!=0;i=p[i].next)
	{
		int v=p[i].to;
		if(!top[v]) dfs2(v,v);
	}
}

struct SGT{
	int tree[M<<2],lazy[M<<2];
	inline void build(int u,int ll,int rr)
	{
		tree[u]=lazy[u]=inf;
		if(ll==rr) return ;
		int mid=(ll+rr)>>1;
		build(u<<1,ll,mid),build(u<<1|1,mid+1,rr);
	}
	inline void pushdown(int u)
	{
		tree[u<<1]=min(tree[u<<1],lazy[u]),tree[u<<1|1]=min(tree[u<<1|1],lazy[u]);
		lazy[u<<1]=min(lazy[u<<1],lazy[u]),lazy[u<<1|1]=min(lazy[u<<1|1],lazy[u]);
		lazy[u]=inf;
	}
	inline void update(int u,int ll,int rr,int L,int R,int k)
	{
		if(L<=ll&&rr<=R)
		{
			tree[u]=min(tree[u],k),lazy[u]=min(lazy[u],k);
			return ;
		}
		if(lazy[u]!=inf) pushdown(u);
		int mid=(ll+rr)>>1;
		if(mid>=L) update(u<<1,ll,mid,L,R,k);
		if(R>mid) update(u<<1|1,mid+1,rr,L,R,k);
		tree[u]=max(tree[u<<1],tree[u<<1|1]);
	}
	inline int query(int u,int ll,int rr,int L,int R)
	{
		if(L<=ll&&rr<=R) return tree[u];
		if(lazy[u]!=inf) pushdown(u);
		int mid=(ll+rr)>>1,res=0;
		if(mid>=L) res=query(u<<1,ll,mid,L,R);
		if(R>mid) res=max(res,query(u<<1|1,mid+1,rr,L,R));
		return res;
	}
	inline int ask(int x,int y)
	{
		int res=0;
		while(top[x]!=top[y])
		{
			if(deep[top[x]]<deep[top[y]]) swap(x,y);
			res=max(res,query(1,1,n,id[top[x]],id[x]));
			x=fa[top[x]];
		}
		if(deep[x]>deep[y]) swap(x,y);
		res=max(res,query(1,1,n,id[x]+1,id[y]));
		return res;
	}
	inline void change(int x,int y,int k)
	{
		while(top[x]!=top[y])
		{
			if(deep[top[x]]<deep[top[y]]) swap(x,y);
			update(1,1,n,id[top[x]],id[x],k);
			x=fa[top[x]];
		}
		if(deep[x]>deep[y]) swap(x,y);
		update(1,1,n,id[x]+1,id[y],k);
	}
};SGT t;
signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	//freopen("P10875_21.in","r",stdin);
	cin>>n>>m;
	for(int i=1;i<=n;++i) F[i]=i;
	for(int i=1,x,y;i<=m;++i)
	{
		cin>>x>>y;
		if(!merge(x,y)) a[++tot]=(node){x,y,i};
		else add(x,y),add(y,x);
	}//先添加树边,多余的边离线下来
	for(int i=1;i<=n;++i)
	{
		if(!siz[i])
		{
			root=i;
			dfs1(i,0,1),dfs2(i,i);
		}//先把得到的树树剖(注意可能是森林)
	}
	t.build(1,1,n);//初始化线段树
	for(int i=1;i<=tot;++i) t.change(a[i].x,a[i].y,a[i].t);//加入多余的边,对树边取min
	cin>>q;int x,y,res;
	while(q--)
	{
		cin>>x>>y;
		if(col[x]!=col[y]) {cout<<"-1\n";continue;}//不连通
		res=t.ask(x,y);
		if(res==inf) cout<<"-1\n";//最后都不边双连通
		else cout<<res<<"\n";
	}
	return 0;
}

后面的题先咕咕咕。

posted @   call_of_silence  阅读(127)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示