前言

WFLS 暑假集训 Day 5 Day 6 Day 8 Day 9

Tarjan 是个巨佬,快来膜拜他 orz。

长文警告:本文一共 \(1092\) 行,请合理安排阅读时间。

强连通分量

强连通分量针对有向图,本篇目内图指有向图。

定义

强连通:如果一个图中任意两点可以相互到达,那么称这个图为强连通的。

极大:在满足条件的情况下包含点数最多的子图。

强连通分量:一个图中的极大强连通子图,叫做强连通分量。

换句话说,在强连通分量中,任意两点可以相互到达。只要到达其中任意一个点,其余的每一个点都可以到达。也就是说,在一个强连通分量里的点可以看作一个点,这就是后面会提到的,也就是 Tarjan 最主要的用处——缩点

一个强连通分量(除一个点)中至少包含一个

Tarjan 算法(核心)

Tarjan 算法定义了两个数组:dfnlow。我们以深度优先搜索的方式遍历这个图,其中 dfn[i] 表示节点 \(i\) 的时间戳low[i] 表示点 \(i\) 可以到达的最小的时间戳,也就是最小的 dfn 值。

首次访问到一个强连通分量里的某个点时,会发现这个点无法回溯到比其 dfn 值更小的节点。否则必然可以通过这个点到达其父节点,构成另一个强连通分量。而 low[i] 的值最小是 dfn[i],因为一个点总是能回溯到自己。所以,当我们发现一个点的 dfnlow 值相等时,就意味着我们找到了一个强连通分量。

Tarjan 算法(实现)

由于缩点需要,仅仅求出有多少强连通分量是不够的,我们需要求出每个点属于哪个强连通分量,从而进行缩点。我们可以发现这样一个性质:首次访问到一个强连通分量里的某个点后,这个强连通分量里的每一个点,都是其在 DFS 搜索树上的子节点。我们可以用一个栈保存其子节点,发现满足条件直接将在这个后进栈,也就是其子节点全部出栈,并及时记录即可。

在整个实现过程中,我们采用 DFS 框架,通过 dfn 的值来反映这个点是否访问过。如果没有访问过,就递归计算这个子节点,回溯时注意更新 low 值,因为父节点可以通过这个子节点进行转移;如果访问过,那么直接更新 low 值即可。

void tarjan(int now)
{
dfn[now]=low[now]=++dfc,st[++top]=now,in[now]=1;
for(int i=h[now];i;i=e[i].next)
if(!dfn[e[i].v])
{
tarjan(e[i].v);
low[now]=min(low[now],low[e[i].v]);
}
else if(in[e[i].v])
low[now]=min(low[now],dfn[e[i].v]);
if(dfn[now]==low[now])
{
sc++;
while(st[top]!=now)scc[st[top]]=sc,in[st[top]]=0,top--;
scc[st[top]]=sc,in[st[top]]=0,top--;
}
}

缩点

将每个点属于的强连通分量计算出来后,可以依据这个进行缩点。具体做法是,遍历每一条边,如果发现这条边连接的两个节点属于不同的强连通分量,那么证明这两个强连通分量有路径连接,在强连通分量编号中建边。

另外,缩点要求在一个强连通分量中的点共享状态。也就是其中之一发生变化,其余的每一个点都要发生同样的变化。

缩点之后的有向图有一个特点:没有环,是 DAG。因为如果有环,一定会是强连通分量,进而被缩成一个点。缩点之后,一些不能处理环的算法,例如拓扑排序,就可以正常使用了。

void add_edge1(int u,int v)
{
e[++cnt1].next=h[u];
e[cnt1].v=v;
h[u]=cnt1;
} void add_edge2(int u,int v)
{
s[++cnt2].next=d[u];
s[cnt2].v=v;
d[u]=cnt2;
} for(int i=1;i<=n;i++)
for(int j=h[i];j;j=e[j].next)
if(scc[i]!=scc[e[j].v])add_edge2(scc[i],scc[e[j].v]);

割边(点)

割边(点)针对无向图,本篇目内图指无向图。

定义(以下定义中图均为无向图)

割点:如果在一个连通图中,删去一个,可以使整个图不再连通,那么这个点叫做割点

割边:如果在一个连通图中,删去一条,可以使整个图不再连通,那么这个点叫做割边

有割点一定有割边,有割边不一定有割点。

例如这个图,就是有割边无有割点。

如果一个图中不存在割点,则任意两个点之间都至少有两条点不重合的路径;如果一个图中不存在割边,则任意两条边之间都至少有两条边不重合的路径。

割点算法(核心)

我们可以套用 Tarjan 算法的定义,在 Tarjan 算法的基础上设计割点/边算法。

如果存在一个点 \(i\),满足这个点 DFS 树上的任一子节点的 low 值大于或等于 \(dfn[i]\),那么这个点就是割点。因为如果这个点的子节点没有办法通过除 \(i\) 以外的其他节点到达点 \(i\) 之前的点,那么删去这个点,这个点的子节点和点 \(i\) 之前的点就不能互相到达,图不再连通,符合割点的定义。

注意这个方法需要特判 DFS 树上的根节点,因为没有任何一个点能到达根节点之前的节点,但是根节点除至少有两个子节点才是割点。

割点算法(实现)

我们额外用一个变量记录当前节点 DFS 树上的儿子个数。遍历每一个儿子节点,如果出现任意一个子节点 low 值大于或等于 \(dfn[i]\),且这个点不为根节点,直接记录为割点。如果是根节点,需要等记录儿子个数的变量大于 \(1\) 时才能判定为割点。

void tarjan(int now,int fa)
{
int chi=0;
dfn[now]=low[now]=++dfc,st[++top]=now,in[now]=1;
for(int i=h[now];i;i=e[i].next)
if(!dfn[e[i].v])
{
tarjan(e[i].v,now);
low[now]=min(low[now],low[e[i].v]);
if(low[e[i].v]>=dfn[now])
{
chi++;
if(now!=root||chi>1)cut[now]=1;
}
}
else if(in[e[i].v]&&e[i].v!=fa)
low[now]=min(low[now],dfn[e[i].v]);
if(dfn[now]==low[now])
{
while(st[top]!=now)in[st[top]]=0,top--;
in[st[top]]=0,top--;
}
}

割边算法(核心)

总体和割边算法一致,但是判定割边的条件为出现任意一个子节点 low 值大于 \(dfn[i]\),不能等于。因为如果等于,证明这个子节点可以越过这条边到达祖先,删去这条边对连通性造成影响。

割边算法(实现)

由于不用特判根节点,实现起来就比割点简单多了。不需要记录儿子数量,只要有一个子节点满足 low 值大于 \(dfn[i]\),即可判定为割边。

void tarjan(int now,int fa)
{
dfn[now]=low[now]=++dfc,st[++top]=now,in[now]=1;
for(int i=h[now];i;i=e[i].next)
if(!dfn[e[i].v]&&e[i].v!=fa)
{
tarjan(e[i].v,now);
low[now]=min(low[now],low[e[i].v]);
if(low[e[i].v]>dfn[now])ans++;
}
else if(in[e[i].v]&&e[i].v!=fa)
low[now]=min(low[now],dfn[e[i].v]);
if(dfn[now]==low[now])
{
while(st[top]!=now)in[st[top]]=0,top--;
in[st[top]]=0,top--;
}
}

双连通分量

双连通分量针对无向图,本篇目内图指无向图。

定义

点双连通分量:如果一个极大子图中不存在割点,那么这个子图为点双连通分量(\(v-DCC\))。

边双连通分量:如果一个极大子图中不存在割边,那么这个子图为边双连通分量(\(e-DCC\))。

像强连通分量在有向图中用于缩点一样,点(边)双连通分量也可以用于在无向图中缩点。缩点之后,图就是一棵

边双连通分量一定是点双连通分量,点双连通分量不一定是边双连通分量。

例如这个图,就是点双连通分量,但不是边双连通分量。

每个点只会属于一个边双连通分量,但可能属于多个点双连通分量。

边双连通分量求法

由于边双连通分量中不存在割边,所以只需要删去所有割边,剩下的图中就不存在割边,剩下的每个连通分量都是边双连通分量。

void tarjan(int now,int fa)
{
dfn[now]=low[now]=++dfc;
for(int i=h[now];i;i=e[i].next)
if(!dfn[e[i].v])
{
tarjan(e[i].v,i);
low[now]=min(low[now],low[e[i].v]);
if(low[e[i].v]>dfn[now])cut[i]=cut[i^1]=1;
}
else if(i!=(fa^1))
low[now]=min(low[now],dfn[e[i].v]);
} void dfs(int now)
{
scc[now]=sc;
scp[sc].push_back(now);
for(int i=h[now];i;i=e[i].next)
{
if(scc[e[i].v]||cut[i])continue;
dfs(e[i].v);
}
} for(int i=1;i<=n;i++)
if(!scc[i])
{
sc++;
dfs(i);
}

点双连通分量(核心)

由于一个点可能会属于多个点双连通分量,所以上述求边双连通分量的方法不能使用了。

我们类比 Tarjan 求强连通分量的算法,用一个栈把每个点存起来。当我们发现一个点是割点时,不断弹栈直到上一次出栈的节点是造成这个点是割点的子节点,最后将这个点加入这个点双连通分量。因为如果一个点是割点,那么这个点会造成这个点到其子节点的下一个割点之间出现一个点双连通分量。按照递归的方式,其子节点的下一个割点之后出现的点已经弹完了,剩余的就是这个点双连通分量的节点了。

当然,这里判断上一次出栈的节点是否为造成这个点是割点的子节点,而不是栈顶节点是否为这个割点,是为了防止这个割点还有其他子树,由于存储结构是栈,这个割点的其他子树的节点也在这个割点之上,按照后者判定会把这些点也加入这个点双连通分量,这并不符合要求。

点双连通分量(实现)

参考 Tarjan 求割点的代码,在递归搜索完所有子节点后,每次判定为是割点时,不断弹栈直到上一次出栈的节点是造成这个点是割点的子节点。

注意,如果这样做,最后栈不为空时,需要把剩余的元素单独加入一个点双连通分量,因为这是根节点没有处理的第一个孩子。

void tarjan(int now,int fa)
{
int chi=0;
dfn[now]=low[now]=++dfc,st[++top]=now;
if(now==root&&h[now]==0)dcc[++dc].push_back(st[top]),top--;
for(int i=h[now];i;i=e[i].next)
if(!dfn[e[i].v])
{
tarjan(e[i].v,i);
low[now]=min(low[now],low[e[i].v]);
if(low[e[i].v]>=dfn[now])
{
chi++;
if(now!=root||chi>1)
{
dc++;
while(st[top+1]!=e[i].v)dcc[dc].push_back(st[top]),top--;
dcc[dc].push_back(now);
}
}
}
else if(i!=(fa^1))
low[now]=min(low[now],dfn[e[i].v]);
}

例题

例题 \(1\) :

P3387 【模板】缩点

缩点模板题,注意记录每个强连通分量的权值。缩完点之后,由于图是一个 DAG,所以直接拓扑排序跑一边最长路即可,不多赘述。

这里的代码实现不是很优美,可以参考其他例题的代码。

#include <bits/stdc++.h>
using namespace std;
struct edge
{
int v,next;
}e[300000],s[300000];
int n,m,u,v,a[300000],h[300000],sh[300000],dfn[300000],low[300000],st[300000],in[300000],scc[300000],q[300000],ind[300000],dis[300000],top=0,cnt=0,dfc=0,sc=0,ans=0;
void add_edge(int u,int v,struct edge e[],int h[])
{
e[++cnt].next=h[u];
e[cnt].v=v;
h[u]=cnt;
} void tarjan(int now)
{
dfn[now]=low[now]=++dfc,st[++top]=now,in[now]=1;
for(int i=h[now];i;i=e[i].next)
if(!dfn[e[i].v])
{
tarjan(e[i].v);
low[now]=min(low[now],low[e[i].v]);
}
else if(in[e[i].v])
low[now]=min(low[now],dfn[e[i].v]);
if(dfn[now]==low[now])
{
sc++;
while(st[top]!=now)
scc[st[top]]=sc,q[sc]+=a[st[top]],in[st[top]]=0,top--;
scc[st[top]]=sc,q[sc]+=a[st[top]],in[st[top]]=0,top--;
}
} void topo_sort()
{
int h=1,t=0,que[300000];
for(int i=1;i<=sc;i++)
if(ind[i]==0)dis[i]=q[i],que[++t]=i;
while(h<=t)
{
int now=que[h];
for(int i=sh[now];i;i=s[i].next)
{
dis[s[i].v]=max(dis[s[i].v],dis[now]+q[s[i].v]);
ind[s[i].v]--;
if(ind[s[i].v]==0)que[++t]=s[i].v;
}
h++;
}
} int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&u,&v);
add_edge(u,v,e,h);
}
for(int i=1;i<=n;i++)
if(!dfn[i])tarjan(i);
cnt=0;
for(int i=1;i<=n;i++)
for(int j=h[i];j;j=e[j].next)
if(scc[i]!=scc[e[j].v])ind[scc[e[j].v]]++,add_edge(scc[i],scc[e[j].v],s,sh);
topo_sort();
for(int i=1;i<=sc;i++)
ans=max(ans,dis[i]);
printf("%d",ans);
return 0;
}

例题 \(2\) :

P2341 [USACO03FALL / HAOI2006] 受欢迎的牛 G

由于图中可能存在环,先缩点,记录每个强连通分量的点数,之后建图。在一个强连通分量中,所有奶牛互相爱慕,只要其中一个发生变化,另外所有的都会发生变化,满足缩点的条件。

如果缩点后图不连通,那么没有任何奶牛能成为明星奶牛。如果一个强连通分量不存在出边,由于图必须连通,所以这个强连通分量只存在入边。如果不存在其他的强连通分量不存在出边,那么所有入边最终都是指向这一个强连通分量,这个强连通分量中的每一头奶牛都是明星。如果存在多于一个强连通分量不存在出边,则这些强连通分量中必然互不指向,没有一头奶牛能成为明星。

总结:让别人喜欢你的方法,就是不喜欢别人。(划掉

#include <bits/stdc++.h>
using namespace std;
struct edge
{
int v,next;
}e[1100000],s[1100000];
int n,m,u,v,h[300000],sh[300000],dfn[300000],low[300000],st[300000],in[300000],scc[300000],q[300000],oud[300000],top=0,cnt=0,dfc=0,sc=0,ans=0;
void add_edge(int u,int v,struct edge e[],int h[])
{
e[++cnt].next=h[u];
e[cnt].v=v;
h[u]=cnt;
} void tarjan(int now)
{
dfn[now]=low[now]=++dfc,st[++top]=now,in[now]=1;
for(int i=h[now];i;i=e[i].next)
if(!dfn[e[i].v])
{
tarjan(e[i].v);
low[now]=min(low[now],low[e[i].v]);
}
else if(in[e[i].v])
low[now]=min(low[now],dfn[e[i].v]);
if(dfn[now]==low[now])
{
sc++;
while(st[top]!=now)
scc[st[top]]=sc,q[sc]++,in[st[top]]=0,top--;
scc[st[top]]=sc,q[sc]++,in[st[top]]=0,top--;
}
} int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&u,&v);
add_edge(u,v,e,h);
}
for(int i=1;i<=n;i++)
if(!dfn[i])tarjan(i);
cnt=0;
for(int i=1;i<=n;i++)
for(int j=h[i];j;j=e[j].next)
if(scc[i]!=scc[e[j].v])oud[scc[i]]++,add_edge(scc[i],scc[e[j].v],s,sh);
for(int i=1;i<=sc;i++)
if(oud[i]==0&&ans==0)ans=q[i];
else if(oud[i]==0)ans=0;
printf("%d",ans);
return 0;
}

例题 \(3\) :

P2812 校园网络【[USACO]Network of Schools加强版】

由于图中可能有环,且一个强连通分量内的点只要到达一个,其余的就也能到达,所以先缩点。

缩点之后,第一问易得结果为入度为 \(0\) 的强连通分量的数量。因为入度为 \(0\) 的强连通分量不可能接受到其他强连通分量的传递,必须自己拥有才行。由于图是个 DAG,所以所有入度为 \(0\) 的强连通分量拥有之后,可以下传到任意一个强连通分量。

第二问,结果为入度为 \(0\) 的强连通分量的数量和出度为 \(0\) 的强连通分量的数量的较大值。根据题目要求,补充完整之后缩点后的整个图是一个强连通分量。让出度为 \(0\) 的强连通分量连向入度为 \(0\) 的强连通分量,刚好构成一个环。每一对这样连接,最终的图一定是由若干个环嵌套而成的。当然,对于每一个成对连接后多出来的入度或出度等于 \(0\) 的强连通分量,需要额外连一条边到一个环中。站在两者之间较多的角度来看,就是每一个点连一条边,就相当于求两者之间的较大值。

#include <bits/stdc++.h>
using namespace std;
struct edge
{
int v,next;
}e[300000];
int n,u,h[300000],dfn[300000],low[300000],st[300000],in[300000],scc[300000],ind[300000],oud[300000],top=0,cnt=0,dfc=0,sc=0,ans=0,ic=0,oc=0;
unordered_map<long long,bool>b;
void add_edge(int u,int v)
{
e[++cnt].next=h[u];
e[cnt].v=v;
h[u]=cnt;
} void tarjan(int now)
{
dfn[now]=low[now]=++dfc,st[++top]=now,in[now]=1;
for(int i=h[now];i;i=e[i].next)
if(!dfn[e[i].v])
{
tarjan(e[i].v);
low[now]=min(low[now],low[e[i].v]);
}
else if(in[e[i].v])
low[now]=min(low[now],dfn[e[i].v]);
if(dfn[now]==low[now])
{
sc++;
while(st[top]!=now)
scc[st[top]]=sc,in[st[top]]=0,top--;
scc[st[top]]=sc,in[st[top]]=0,top--;
}
} int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
u=-1;
while(u!=0)
{
scanf("%d",&u);
if(u==0)break;
add_edge(i,u);
}
}
for(int i=1;i<=n;i++)
if(!dfn[i])tarjan(i);
for(int i=1;i<=n;i++)
for(int j=h[i];j;j=e[j].next)
if(scc[i]!=scc[e[j].v]&&!b[(long long)scc[i]*20000+e[j].v])
b[(long long)scc[i]*20000+e[j].v]=1,oud[scc[i]]++,ind[scc[e[j].v]]++;
for(int i=1;i<=sc;i++)
if(ind[i]==0)ans++;
for(int i=1;i<=sc;i++)
{
if(ind[i]==0)ic++;
if(oud[i]==0)oc++;
}
if(sc==1)ic=0,oc=0;
printf("%d\n%d",ans,max(ic,oc));
return 0;
}

例题 \(4\) :

P2403 [SDOI2010] 所驼门王的宝藏

对于没有宝藏的宫室,完全没有去的必要,不需要考虑。这样,点的数量就降低到了 \(N\) 个。

考虑点与点之间连边。对于任意门,每次最多只会连 \(8\) 条边,直接哈希之后暴力连边。对于横天门,我们在每一行中选出一个横天门“代表门”,同一行中的其他横天门与这个“代表门”之间连双向边,其他非横天门与这个“代表门”之间连“代表门”到其他门的单向边。纵寰门也是同理。

注意这里的连边必须时读入完所有点之后在进行,否则可能会出现部分点没有连边的情况。

连完边之后,考虑缩点,之后图就是一个 DAG,直接用拓扑排序求最长路即可(就是例题 \(1\))。

#include <bits/stdc++.h>
using namespace std;
struct edge
{
int v,next;
}e[300000],s[300000];
int n,r,c,u[300000],v[300000],t[300000],h[300000],sh[300000],dfn[300000],low[300000],st[300000],in[300000],scc[300000],q[300000],ind[300000],dis[300000],dh[300000],dl[300000],top=0,cnt=0,dfc=0,sc=0,ans=0;
int dx[8]={0,0,1,-1,1,1,-1,-1};
int dy[8]={1,-1,0,0,1,-1,1,-1};
unordered_map<long long,int>hou;
void add_edge(int u,int v,struct edge e[],int h[])
{
e[++cnt].next=h[u];
e[cnt].v=v;
h[u]=cnt;
} void tarjan(int now)
{
dfn[now]=low[now]=++dfc,st[++top]=now,in[now]=1;
for(int i=h[now];i;i=e[i].next)
if(!dfn[e[i].v])
{
tarjan(e[i].v);
low[now]=min(low[now],low[e[i].v]);
}
else if(in[e[i].v])
low[now]=min(low[now],dfn[e[i].v]);
if(dfn[now]==low[now])
{
sc++;
while(st[top]!=now)
scc[st[top]]=sc,q[sc]++,in[st[top]]=0,top--;
scc[st[top]]=sc,q[sc]++,in[st[top]]=0,top--;
}
} void topo_sort()
{
int h=1,t=0,que[300000];
for(int i=1;i<=sc;i++)
if(ind[i]==0)dis[i]=q[i],que[++t]=i;
while(h<=t)
{
int now=que[h];
for(int i=sh[now];i;i=s[i].next)
{
dis[s[i].v]=max(dis[s[i].v],dis[now]+q[s[i].v]);
ind[s[i].v]--;
if(ind[s[i].v]==0)que[++t]=s[i].v;
}
h++;
}
} long long makepair(int x,int y)
{
return (long long)x*200000+y;
} int main()
{
scanf("%d%d%d",&n,&r,&c);
for(int i=1;i<=n;i++)
{
scanf("%d%d%d",&u[i],&v[i],&t[i]);
if(t[i]==1&&dh[u[i]]==0)dh[u[i]]=i;
if(t[i]==1&&dh[u[i]]!=0)add_edge(i,dh[u[i]],e,h);
if(t[i]==2&&dl[v[i]]==0)dl[v[i]]=i;
if(t[i]==2&&dl[v[i]]!=0)add_edge(i,dl[v[i]],e,h);
if(t[i]==3)hou[makepair(u[i],v[i])]=i;
}
for(int i=1;i<=n;i++)
{
if(dh[u[i]]!=0)add_edge(dh[u[i]],i,e,h);
if(dl[v[i]]!=0)add_edge(dl[v[i]],i,e,h);
for(int j=0;j<8;j++)
if(hou[makepair(u[i]+dx[j],v[i]+dy[j])]!=0)add_edge(hou[makepair(u[i]+dx[j],v[i]+dy[j])],i,e,h);
}
for(int i=1;i<=n;i++)
if(!dfn[i])tarjan(i);
cnt=0;
for(int i=1;i<=n;i++)
for(int j=h[i];j;j=e[j].next)
if(scc[i]!=scc[e[j].v])ind[scc[e[j].v]]++,add_edge(scc[i],scc[e[j].v],s,sh);
topo_sort();
for(int i=1;i<=sc;i++)
ans=max(ans,dis[i]);
printf("%d",ans);
return 0;
}

例题 \(5\) :

[USACO15JAN] Grass Cownoisseur G

为了避免环的影响,先缩点。然后在缩点后的图进行记忆化搜索。还需要反向存一次边,方便计算逆行时的结果。设计状态 \(f[i][j][k]\),表示从编号为 \(j\) 的点搜索到编号为 \(i\) 的点,是否逆行过(\(k=0\) 表示没有,\(k=1\) 表示有)走到的最多的点数。

转移时,类似普通遍历图,记录当前节点 \(i\) 和前驱节点 \(j\),递归搜索 \(i\) 除 \(j\) 以外的每一个邻接点,取状态最大值,加上这个强连通分量的点的数量。如果一个状态的 \(k\) 值为 \(0\),还需要额外处理逆行时的情况,同样不能走回 \(j\) 点,取最大值。

需要注意的是,这样处理逆行是不会导致某个强连通分量重复计算的。假设某个点通过走回了一个计算过的点,那么在计算这个计算过的点时,就会直接搜索这个逆行点。故这个点必然是逆行点的前驱,而逆行转移是不会走回前驱点的,假设不成立。当然,在图中存在环时,也可能会有种情况。但是缩点之后图是一个 DAG,不存在环,依旧不成立。

状态中的 \(i,j\) 分别记录可能爆空间,仔细分析下来发现 \(i,j\) 其实是边上的一对点,相当于一条边。如果把 \((i,j)\) 作为一个数对哈希出来,状态数量其实就只与边数有关,不会爆空间。

#include <bits/stdc++.h>
using namespace std;
struct edge
{
int v,next,dis;
}e[300000],s[300000];
int n,m,u,v,h[300000],sh[300000],dfn[300000],low[300000],st[300000],in[300000],scc[300000],q[300000],top=0,cnt=0,dfc=0,sc=0;
unordered_map<long long,int>f[2];
void add_edge(int u,int v,int dis,struct edge e[],int h[])
{
e[++cnt].next=h[u];
e[cnt].v=v;
e[cnt].dis=dis;
h[u]=cnt;
} void tarjan(int now)
{
dfn[now]=low[now]=++dfc,st[++top]=now,in[now]=1;
for(int i=h[now];i;i=e[i].next)
if(!dfn[e[i].v])
{
tarjan(e[i].v);
low[now]=min(low[now],low[e[i].v]);
}
else if(in[e[i].v])
low[now]=min(low[now],dfn[e[i].v]);
if(dfn[now]==low[now])
{
sc++;
while(st[top]!=now)
scc[st[top]]=sc,q[sc]++,in[st[top]]=0,top--;
scc[st[top]]=sc,q[sc]++,in[st[top]]=0,top--;
}
} long long makepair(int x,int y)
{
return (long long)x*200000+y;
} int dfs(int now,int pre,int used)
{
int mx=-99999999;
if(now==scc[1]&&pre!=0)return 0;
if(f[used][makepair(now,pre)])return f[used][makepair(now,pre)];
for(int i=sh[now];i;i=s[i].next)
if(s[i].v!=pre)
{
if(s[i].dis==0)mx=max(mx,dfs(s[i].v,now,used));
else if(s[i].dis==1&&used==0)mx=max(mx,dfs(s[i].v,now,1));
}
f[used][makepair(now,pre)]=mx+q[now];
return mx+q[now];
} int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&u,&v);
add_edge(u,v,0,e,h);
}
for(int i=1;i<=n;i++)
if(!dfn[i])tarjan(i);
cnt=0;
for(int i=1;i<=n;i++)
for(int j=h[i];j;j=e[j].next)
if(scc[i]!=scc[e[j].v])add_edge(scc[i],scc[e[j].v],0,s,sh),add_edge(scc[e[j].v],scc[i],1,s,sh);
printf("%d",dfs(scc[1],0,0));
return 0;
}

例题 \(6\) :

P3388 【模板】割点(割顶)

割点模板题,不多赘述。

#include <bits/stdc++.h>
using namespace std;
struct edge
{
int v,next;
}e[300000];
int n,m,u,v,root=0,h[300000],dfn[300000],low[300000],st[300000],in[300000],cut[300000],top=0,cnt=0,dfc=0,ans=0;
void add_edge(int u,int v)
{
e[++cnt].next=h[u];
e[cnt].v=v;
h[u]=cnt;
} void tarjan(int now,int fa)
{
int chi=0;
dfn[now]=low[now]=++dfc,st[++top]=now,in[now]=1;
for(int i=h[now];i;i=e[i].next)
if(!dfn[e[i].v])
{
tarjan(e[i].v,now);
low[now]=min(low[now],low[e[i].v]);
if(low[e[i].v]>=dfn[now])
{
chi++;
if(now!=root||chi>1)cut[now]=1;
}
}
else if(in[e[i].v]&&e[i].v!=fa)
low[now]=min(low[now],dfn[e[i].v]);
if(dfn[now]==low[now])
{
while(st[top]!=now)in[st[top]]=0,top--;
in[st[top]]=0,top--;
}
} int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&u,&v);
add_edge(u,v);add_edge(v,u);
}
for(int i=1;i<=n;i++)
if(!dfn[i])root=i,tarjan(i,0);
for(int i=1;i<=n;i++)
if(cut[i])ans++;
printf("%d\n",ans);
for(int i=1;i<=n;i++)
if(cut[i])printf("%d ",i);
return 0;
}

例题 \(7\) :

T103481 【模板】割边

割边模板题,不多赘述。

#include <bits/stdc++.h>
using namespace std;
struct edge
{
int v,next;
}e[750000];
int n,m,u,v,root=0,h[300000],dfn[300000],low[300000],st[300000],in[300000],top=0,cnt=0,dfc=0,ans=0;
void add_edge(int u,int v)
{
e[++cnt].next=h[u];
e[cnt].v=v;
h[u]=cnt;
} void tarjan(int now,int fa)
{
dfn[now]=low[now]=++dfc,st[++top]=now,in[now]=1;
for(int i=h[now];i;i=e[i].next)
if(!dfn[e[i].v]&&e[i].v!=fa)
{
tarjan(e[i].v,now);
low[now]=min(low[now],low[e[i].v]);
if(low[e[i].v]>dfn[now])ans++;
}
else if(in[e[i].v]&&e[i].v!=fa)
low[now]=min(low[now],dfn[e[i].v]);
if(dfn[now]==low[now])
{
while(st[top]!=now)in[st[top]]=0,top--;
in[st[top]]=0,top--;
}
} int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&u,&v);
add_edge(u,v);add_edge(v,u);
}
for(int i=1;i<=n;i++)
if(!dfn[i])root=i,tarjan(i,0);
printf("%d\n",ans);
return 0;
}

例题 \(8\) :

P8436 【模板】边双连通分量

边双连通分量模板题,不多赘述。注意重边的处理。

#include <bits/stdc++.h>
using namespace std;
struct edge
{
int v,next;
}e[20040000];
int n,m,u,v,h[2000010],dfn[2000010],low[2000010],cut[20040000],scc[2000010],top=0,cnt=1,dfc=0,sc=0;
vector<int>scp[2000010];
void add_edge(int u,int v)
{
e[++cnt].next=h[u];
e[cnt].v=v;
h[u]=cnt;
} void tarjan(int now,int fa)
{
dfn[now]=low[now]=++dfc;
for(int i=h[now];i;i=e[i].next)
if(!dfn[e[i].v])
{
tarjan(e[i].v,i);
low[now]=min(low[now],low[e[i].v]);
if(low[e[i].v]>dfn[now])cut[i]=cut[i^1]=1;
}
else if(i!=(fa^1))
low[now]=min(low[now],dfn[e[i].v]);
} void dfs(int now)
{
scc[now]=sc;
scp[sc].push_back(now);
for(int i=h[now];i;i=e[i].next)
{
if(scc[e[i].v]||cut[i])continue;
dfs(e[i].v);
}
} int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&u,&v);
add_edge(u,v);add_edge(v,u);
}
for(int i=1;i<=n;i++)
if(!dfn[i])tarjan(i,0);
for(int i=1;i<=n;i++)
if(!scc[i])
{
sc++;
dfs(i);
}
printf("%d\n",sc);
for(int i=1;i<=sc;i++)
{
int l=scp[i].size();
printf("%d ",l);
for(int j=0;j<l;j++)
printf("%d ",scp[i][j]);
printf("\n");
}
return 0;
}

例题 \(9\) :

P8435 【模板】点双连通分量

点双连通分量模板题,不多赘述。注意重边的处理。

#include <bits/stdc++.h>
using namespace std;
struct edge
{
int v,next;
}e[20040000];
int n,m,u,v,root=0,h[2000010],dfn[2000010],low[2000010],st[2000010],cut[2000010],top=0,cnt=1,dfc=0,ans=0,dc=0;
vector<int>dcc[2000010];
void add_edge(int u,int v)
{
e[++cnt].next=h[u];
e[cnt].v=v;
h[u]=cnt;
} void tarjan(int now,int fa)
{
int chi=0;
dfn[now]=low[now]=++dfc,st[++top]=now;
if(now==root&&h[now]==0)dcc[++dc].push_back(st[top]),top--;
for(int i=h[now];i;i=e[i].next)
if(!dfn[e[i].v])
{
tarjan(e[i].v,i);
low[now]=min(low[now],low[e[i].v]);
if(low[e[i].v]>=dfn[now])
{
chi++;
if(now!=root||chi>1)
{
dc++;
while(st[top+1]!=e[i].v)dcc[dc].push_back(st[top]),top--;
dcc[dc].push_back(now);
}
}
}
else if(i!=(fa^1))
low[now]=min(low[now],dfn[e[i].v]);
} int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&u,&v);
if(u==v)continue;
add_edge(u,v);add_edge(v,u);
}
for(int i=1;i<=n;i++)
if(!dfn[i])
{
root=i,tarjan(i,0);
if(top!=0)
{
dc++;
while(top>0)dcc[dc].push_back(st[top]),top--;
}
}
printf("%d\n",dc);
for(int i=1;i<=dc;i++)
{
int l=dcc[i].size();
printf("%d ",l);
for(int j=0;j<l;j++)
printf("%d ",dcc[i][j]);
printf("\n");
}
return 0;
}

例题 \(10\) :

P3225 [HNOI2012] 矿场搭建

任意一个点坍塌,其余任意一个点都有逃生的路口,自然联想到割点。在一个点双连通分量中,只有删去割点才会影响图的连通性。

如果一个点双连通分量里存在两个割点,那么其中一个割点坍塌后,图中所有的可以通过另一个割点逃往其他的点双连通分量。如果一个点双连通分量里只存在一个割点,那么这个割点坍塌后,这个点双连通分量就与整个图不再连通,这个点双连通分量内需要建一个逃生口。如果一个点双连通分量里一个割点都没有,那么这个点双连通分量需要两个逃生口,以免其中一个逃生口坍塌。

至于方案数的计算,根据乘法原理,每存在一个只存在一个割点的点双连通分量,把方案数累乘这个点双连通分量除这个割点外的点数。每存在一个没有割点的点双连通分量,那么就把方案数累乘在这个点双连通分量中选择两个不重复点的方案数。

#include <bits/stdc++.h>
using namespace std;
struct edge
{
int v,next;
}e[24000],s[24000];
int t,n,m,u,v,h[20010],d[20010],dfn[20010],low[20010],st[20010],book[20010],b[20010],g[20010],cut[20010],top=0,cnt1=1,cnt2=1,dfc=0,dc=0;
vector<int>dcc[20010],dcd[20010];
void add_edge1(int u,int v)
{
e[++cnt1].next=h[u];
e[cnt1].v=v;
h[u]=cnt1;
} void add_edge2(int u,int v)
{
s[++cnt2].next=d[u];
s[cnt2].v=v;
d[u]=cnt2;
b[u]++;b[v]++;
} void init()
{
memset(h,0,sizeof(h));memset(d,0,sizeof(d));memset(dfn,0,sizeof(dfn));memset(low,0,sizeof(low));
memset(book,0,sizeof(book));memset(b,0,sizeof(b));memset(g,0,sizeof(g));memset(cut,0,sizeof(cut));memset(st,0,sizeof(st));
cnt1=1,cnt2=1,dfc=0,n=0,dc=0,top=0;
} void tarjan1(int now,int fa)
{
int chi=0;
dfn[now]=low[now]=++dfc,st[++top]=now;
for(int i=h[now];i;i=e[i].next)
if(!dfn[e[i].v])
{
tarjan1(e[i].v,i);
low[now]=min(low[now],low[e[i].v]);
if(low[e[i].v]>=dfn[now])
{
chi++;
if(now!=1||chi>1)
{
dc++;
cut[now]=1;
while(st[top+1]!=e[i].v)dcc[st[top]].push_back(dc),dcd[dc].push_back(st[top]),top--;
dcc[now].push_back(dc),dcd[dc].push_back(now);
}
}
}
else if(i!=(fa^1))
low[now]=min(low[now],dfn[e[i].v]);
} void dfs(int now)
{
int l=dcc[now].size();
book[now]=1;
for(int i=0;i<l-1;i++)add_edge2(dcc[now][l-1],dcc[now][i]),add_edge2(dcc[now][i],dcc[now][l-1]);
for(int i=h[now];i;i=e[i].next)
if(!book[e[i].v])dfs(e[i].v);
} int main()
{
while(scanf("%d",&m))
{
if(m==0)break;
t++;
init();
long long ans1=0,ans2=1;
for(int i=1;i<=m;i++)
{
scanf("%d%d",&u,&v);
if(u==v)continue;
n=max(n,max(u,v));
add_edge1(u,v);add_edge1(v,u);
}
tarjan1(1,0);
if(top!=0)
{
dc++;
while(top>0)dcc[st[top]].push_back(dc),dcd[dc].push_back(st[top]),top--;
}
dfs(1);
for(int i=1;i<=n;i++)
if(cut[i])
{
int l=dcc[i].size();
for(int j=0;j<l;j++)g[dcc[i][j]]++;
}
for(int i=1;i<=dc;i++)
if(b[i]==2||g[i]==1)
{
long long cnt=0;
ans1++;
int l=dcd[i].size();
for(int j=0;j<l;j++)
if(!cut[dcd[i][j]])cnt++;
ans2*=cnt;
}
if(ans1==0)ans1=2,ans2=(long long)n*(n-1)/2;
printf("Case %d: %lld %lld\n",t,ans1,ans2);
for(int i=1;i<=n;i++)dcc[i].clear();
for(int i=1;i<=dc;i++)dcd[i].clear();
}
return 0;
}

后记

这篇成功取代 【6】树状数组学习笔记,共 \(1092\) 行,成为最长的学习笔记。

upd on \(2023.12.7\) 被 【7】同余学习笔记 取代。

【7】Tarjan学习笔记的更多相关文章

  1. [Tarjan 学习笔记](无向图)

    今天考试因为不会敲 Dcc 的板子导致没有AK(还不是你太菜了),所以特地写一篇博客记录 Tarjan 的各种算法 无向图的割点与桥 (各种定义跳过) 割边判定法则 无向边 (x,y) 是桥,当且仅当 ...

  2. Tarjan学习笔记

    \(Tarjan\)是个很神奇的算法. 给一张有向图,将其分解成强连通分量们. 强连通分量的定义:一个点集,使得里面的点两两可以互相到达,并且再加上另一个点都无法满足强连通性. \(Tarjan\)的 ...

  3. $tarjan$简要学习笔记

    $QwQ$因为$gql$的$tarjan$一直很差所以一直想着要写个学习笔记,,,咕了$inf$天之后终于还是写了嘻嘻. 首先说下几个重要数组的基本定义. $dfn$太简单了不说$QwQ$ 但是因为有 ...

  4. 仙人掌&圆方树学习笔记

    仙人掌&圆方树学习笔记 1.仙人掌 圆方树用来干啥? --处理仙人掌的问题. 仙人掌是啥? (图片来自于\(BZOJ1023\)) --也就是任意一条边只会出现在一个环里面. 当然,如果你的图 ...

  5. OI知识点|NOIP考点|省选考点|教程与学习笔记合集

    点亮技能树行动-- 本篇blog按照分类将网上写的OI知识点归纳了一下,然后会附上蒟蒻我的学习笔记或者是我认为写的不错的专题博客qwqwqwq(好吧,其实已经咕咕咕了...) 基础算法 贪心 枚举 分 ...

  6. 算法学习笔记(5): 最近公共祖先(LCA)

    最近公共祖先(LCA) 目录 最近公共祖先(LCA) 定义 求法 方法一:树上倍增 朴素算法 复杂度分析 方法二:dfs序与ST表 初始化与查询 复杂度分析 方法三:树链剖分 DFS序 性质 重链 重 ...

  7. js学习笔记:webpack基础入门(一)

    之前听说过webpack,今天想正式的接触一下,先跟着webpack的官方用户指南走: 在这里有: 如何安装webpack 如何使用webpack 如何使用loader 如何使用webpack的开发者 ...

  8. PHP-自定义模板-学习笔记

    1.  开始 这几天,看了李炎恢老师的<PHP第二季度视频>中的“章节7:创建TPL自定义模板”,做一个学习笔记,通过绘制架构图.UML类图和思维导图,来对加深理解. 2.  整体架构图 ...

  9. PHP-会员登录与注册例子解析-学习笔记

    1.开始 最近开始学习李炎恢老师的<PHP第二季度视频>中的“章节5:使用OOP注册会员”,做一个学习笔记,通过绘制基本页面流程和UML类图,来对加深理解. 2.基本页面流程 3.通过UM ...

  10. 2014年暑假c#学习笔记目录

    2014年暑假c#学习笔记 一.C#编程基础 1. c#编程基础之枚举 2. c#编程基础之函数可变参数 3. c#编程基础之字符串基础 4. c#编程基础之字符串函数 5.c#编程基础之ref.ou ...

随机推荐

  1. 🎀GitHub Pages静态文件发布

    简介 GitHub Pages是GitHub提供的一项服务,允许用户和组织从存储库中的静态文件创建和托管网站.这些静态文件可以是HTML.CSS.JavaScript文件或任何其他可以在浏览器中直接渲 ...

  2. 快速理解 MCP 与 A2A 协议的关系,它们如何协作构建复杂AI系统

    近期关于MCP协议的讨论非常热门,主要因为通过MCP协议通过标准化接口为 AI 应用与外部数据源建立统一交互通道,这使得大模型可以与外部数据源或工具进行交互,从而实现各种专业场景下的智能应用.关于如何 ...

  3. PC端网页/web通过自定义协议唤起启动windows桌面应用

    PC端网页/web通过自定义协议唤起启动windows桌面应用 步骤: 写注册表 调用 Windows Registry Editor Version 5.00 [HKEY_CURRENT_USER\ ...

  4. tar命令打包指定目录及其文件,而不包括其上级目录

    想指定将/var目录下的log目录及其文件打包到当前目录,在压缩包解压时不包括/var目录,可使用如下方式: tar -zcvf log_bak.tar.gz -C /var/ log # 注意log ...

  5. 系统自动化DTO实施流程简图

  6. Font Awesome文档使用手册

    Font Awesome 字体为您提供可缩放矢量图标,它可以被定制大小.颜色.阴影以及任何可以用CSS的样式. 使用文档:https://fa4.uihtm.com/ Font Awesome 是一套 ...

  7. 【SQL周周练】:利用行车轨迹分析犯罪分子作案地点

    大家好,我是"蒋点数分",多年以来一直从事数据分析工作.从今天开始,与大家持续分享关于数据分析的学习内容. 本文是第 7 篇,也是[SQL 周周练]系列的第 6 篇.该系列是挑选或 ...

  8. TEA密码与逆向工程

    前置信息 TEA系列概述:TEA算法是一种分组密码算法,‌由剑桥大学计算机实验室的David Wheeler和Roger Needham于1994年发明.‌它使用64位的明文分组和128位的密钥进行加 ...

  9. 在LLVM中的greedy Register Allocation pass代码详解

    LLVM 贪婪寄存器分配器(RAGreedy)详细处理流程 日期: 2025年5月29日 摘要 本文深入分析 LLVM 贪婪寄存器分配器(RAGreedy)的处理流程,详细描述从优先级队列获取虚拟寄存 ...

  10. PLsql and 汉化包的安装使用

    一.准备工作 1.点击下载PLSQL,本次安装的PLSQL版本为12.0.7,建议安装64位. 2.下载PLSQL时,版本旁边会有个"Language pack"的链接,点击后左侧 ...