连通性相关

强联通分量

强连通:有向图 \(G\) 强连通表示,\(G\) 中任意两个结点连通。

强连通分量( Strongly Connected Components ,简称 \(\operatorname{SCC}\) ):极大的 强连通子图。

Tarjan

维护了以下两个变量:

  • \(\texttt{dfn}\):深度优先搜索遍历时结点 \(u\) 被搜索的次序 。
  • \(\texttt{low}\):以 \(u\) 为根的子树的节点、至多通过一条返祖边能够到达的节点的 \(\texttt{dfn}\) 最小值。

从根开始的一条路径上的 \(\texttt{dfn}\) 严格递增,\(\texttt{low}\) 严格非降。

对于一个连通分量图,有且仅有一个 \(dfn(u)=low(u)\) 。该结点一定是在深度遍历的过程中,该连通分量中第一个被访问过的结点,因为它的 \(dfn\) 值和 \(low\) 值最小,不会被该连通分量中的其他结点所影响。

因此,在回溯的过程中,判定 \(dfn(u)=low(u)\) 的条件是否成立,如果成立,则栈中从 后面的结点构成一个 \(\operatorname{SCC}\)

P2341 [HAOI2006]受欢迎的牛 G \(-\) 模板

$\texttt{code}$
#define Maxn 10005
#define Maxm 50005
void tarjan(int u)
{
	 dfn[u]=low[u]=++Time; s.push(u),ins[u]=true;
	 for(int i=hea[u];i;i=nex[i])
	 {
	 	 if(!dfn[ver[i]]) tarjan(ver[i]),low[u]=min(low[ver[i]],low[u]);
		 else if(ins[ver[i]]) low[u]=min(dfn[ver[i]],low[u]);
	 }
	 if(dfn[u]==low[u])
	 {
	 	 sum+=1;
	 	 do
	 	 {
	 	 	 belong[u]=sum;
	 	 	 u=s.top(); s.pop(); ins[u]=false;
	 	 	 cnt[sum]+=1;
		 } while(dfn[u]!=low[u]);
	 }
}

for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);

时间复杂度 \(O(n+m)\)

Kosaraju

复杂度 \(O(n+m)\)

Garbow

复杂度 \(O(n+m)\)

我们可以利用强联通分量将一张图的每个强连通分量都缩成一个点。

然后这张图会变成一个 \(\operatorname{DAG}\),可以进行拓扑排序以及更多其他操作 。

应用 \(-\) 缩点

P3387 【模板】缩点

for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
for(int i=1;i<=tot[0];i++)
	 if(belong[fro[0][i]]!=belong[ver[0][i]])
	 	 add(1,belong[fro[0][i]],belong[ver[0][i]]),ind[belong[ver[0][i]]]++;
topo();

tarjan 求 LCA

tarjan 求 LCA 可以实现均摊 \(O(1)\)

就是用 tarjan 按照顺序遍历子树的特点加上并查集即可。

$\texttt{code}$
inline void add_edge(int x,int y){ ver[++tot]=y,nex[tot]=hea[x],hea[x]=tot; }
inline void add_query(int x,int y,int d)
	 { qver[++qtot]=y,qnex[qtot]=qhea[x],qhea[x]=qtot,qid[qtot]=d; }
int Find(int x){ return (fa[x]==x)?x:(fa[x]=Find(fa[x])); }
void tarjan(int x,int F)
{
	 vis[x]=true;
	 for(int i=hea[x];i;i=nex[i])
	 {
	 	 if(ver[i]==F) continue;
	 	 tarjan(ver[i],x),fa[ver[i]]=x;
	 }
	 for(int i=qhea[x];i;i=qnex[i])
	 {
	 	 if(!vis[qver[i]]) continue;
	 	 ans[qid[i]]=Find(qver[i]);
	 }
}
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1,x,y;i<n;i++) x=rd(),y=rd(),add_edge(x,y),add_edge(y,x);
for(int i=1,x,y;i<=m;i++)
	 x=rd(),y=rd(),add_query(x,y,i),add_query(y,x,i);
tarjan(s,s);
for(int i=1;i<=m;i++) printf("%d\n",ans[i]);

割点与桥

在无向图中删去这个点 \(/\) 边会使极大强联通增大,那么这个点 \(/\) 边为割点 \(/\) 桥 。

注意这里的 \(dfn\) 表示不经过父亲,能到达的最小的 \(dfn\)

割点

P3388 【模板】割点(割顶)

关键条件:

  • \(u\) 是根节点,当至少存在 \(2\) 条边满足 \(low(v)\ge dfn(u)\)\(u\) 是割点 。
  • \(u\) 不是根节点,当至少存在 \(1\) 条边满足 \(low(v)\ge dfn(u)\)\(u\) 是割点 。
$\texttt{code}$
void tarjan(int u,int fa)
{
	 dfn[u]=low[u]=++Time;
	 for(int i=hea[u];i;i=nex[i])
	 {
	 	 if(!dfn[ver[i]])
		 {
		 	 tarjan(ver[i],u),low[u]=min(low[ver[i]],low[u]);
		 	 if(low[ver[i]]>=dfn[u]) cnt[u]+=1;
		 }
		 else if(ver[i]!=fa) low[u]=min(dfn[ver[i]],low[u]);
	 }
}

for(int i=1;i<=n;i++) if(!dfn[i]) cnt[i]-=1,tarjan(i,0);
for(int i=1;i<=n;i++) if(cnt[i]>=1) ans+=1;

割边(桥)

关键条件:

  • 当存在一条边条边满足 \(low(v)>dfn(u)\) 则边 \(i\) 是割边

关键部分的代码:

注意:记录上一个访问的边时要记录边的编号,不能记录上一个过来的节点(因为会有重边)!!!

$\tt{code}$
void tarjan(int x,int Last_edg)
{
	 dfn[x]=low[x]=++Time;
	 for(int i=hea[x];i;i=nex[i])
	 {
	 	 if(!dfn[ver[i]])
	 	 {
	 	 	 tarjan(ver[i],i);
	 	 	 low[x]=min(low[x],low[ver[i]]);
	 	 	 if(low[ver[i]]>dfn[x]) edg[i]=edg[i^1]=1;
		 }
		 else if(i!=(Last_edg^1)) low[x]=min(low[x],dfn[ver[i]]);
	 }
}

for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,0);
for(int j=2;j<=tot;j+=2) ans+=tag[j];

双联通分量

边双联通分量

显然,找出每一个桥,去掉这些桥之后的每一个联通块都是一个边双联通字图。

注意:用边双缩点的时候先处理出割边,之后用 dfs 求出每一个双联通分量,不用栈!!

P2860 [USACO06JAN]Redundant Paths G

要将原图转化为边双联通图需要添加的最少边数

我们可以先将所有的桥找出来,并同时对所有边双缩点,会得到一颗缩完点的、由桥构成的“树”。

我们发现这棵“树”上“叶子结点”的个数除二向上取整就是需要添加的边的条数。

点双连通分量

详见圆方树

例题

P2272 [ZJOI2007]最大半连通子图

我们先把所有强联通分量缩点,那么这些点一定一起选入,半联通子图的充要条件变为:

  • 所有点的入度为 \(1\)
  • 联通。

考虑将边反过来连,那么每个点出度最大为 \(1\)

最大的子图一定是一条完整的链。

只要记录到达一个点最大值以及种数就做完啦!(拓扑)

P3225 [HNOI2012]矿场搭建

首先想到如果删去一个割点,那么总共的连通块数量加一,所以删去后每个连通块都需要设置一个逃生出口,发现只需要在“叶子”连通块内设置出口。

发现不会了,去看题解:发现只要判断这个双联通分量的度数即可。

如果这个双联通分量度数为 \(1\),那么必须设置一个出口。

特殊情况:如果只有一个双联通分量,那么必须再设置一个,放置出口坍塌。

如果只进行一遍 Tarjan,无法判断和根直接相连的双联通分量,所以要两边查找,一遍找出割点,第二遍计算度数。

\(\texttt{code}\)

void tarjan(int x)
{
	 dfn[x]=low[x]=++Time,sta[++tp]=x,n++;
	 int cnt=0;
	 for(int i=hea[x];i;i=nex[i])
	 {
	 	 if(!dfn[ver[i]])
	 	 {
	 	 	 tarjan(ver[i]),low[x]=min(low[x],low[ver[i]]);
	 	 	 if(low[ver[i]]>=dfn[x]) cnt++,isdian[x]=true;
		 }
		 else low[x]=min(low[x],dfn[ver[i]]);
	 }
	 if(x==rt && cnt==1) isdian[x]=false;
}
void Find(int x,int bel)
{
	 siz[bel]++,vis[x]=true;
	 for(int i=hea[x];i;i=nex[i]) if(!vis[ver[i]])
	 {
	 	 if(isdian[ver[i]])
		 {
		 	 if(!ppp[ver[i]]) ind[bel]++,ppp[ver[i]]=true,sta[++tp]=ver[i];
			 continue;
		 }
	 	 Find(ver[i],bel);
	 }
}
int main()
{
	 int T=0;
	 while((m=rd())>0)
	 {
	 	 n=tot=Time=sum=hav=0,ans=1,T++;
	 	 memset(exist,false,sizeof(exist));
	 	 memset(isdian,false,sizeof(isdian));
	 	 memset(vis,false,sizeof(vis));
	 	 memset(hea,0,sizeof(hea));
	 	 memset(dfn,0,sizeof(dfn));
	 	 memset(siz,0,sizeof(siz));
	 	 memset(ind,0,sizeof(ind));
	 	 for(int i=1,x,y;i<=m;i++)
	 	 	 x=rd(),y=rd(),add(x,y),add(y,x),exist[x]=exist[y]=true;
	 	 for(int i=1;i<=1000;i++) if(exist[i] && !dfn[i])
		  	 tp=0,rt=i,tarjan(i);
	 	 for(int i=1;i<=1000;i++) if(exist[i] && !isdian[i] && !vis[i])
	 	 {
	 	 	 tp=0,sum++,Find(i,sum);
	 	 	 while(tp) ppp[sta[tp--]]=false;
		 }
	 	 if(sum==1)
	 	 	 printf("Case %d: %d %lld\n",T,2,1ll*n*(n-1)/2);
	 	 else
	 	 {
	 	 	 for(int i=1;i<=sum;i++) if(ind[i]==1)
	 	 	 	 hav++,ans*=1ll*siz[i];
	 	 	 printf("Case %d: %d %lld\n",T,hav,ans);
		 }
	 }
	 return 0;
}

3.4 [USACO17DEC]Push a Box P

难以置信这竟然是连通性相关!!

咕咕咕

posted @ 2021-10-06 20:23  EricQian06  阅读(69)  评论(0编辑  收藏  举报