(Day18)算法复健运动for蓝桥杯-图的强连通-tarjan算法
(Day18)算法复健运动for蓝桥杯-图的强连通-tarjan算法
1. 图的强连通
来源博客:https://blog.csdn.net/mengxiang000000/article/details/51672725?locationNum=10&fps=1
https://www.cnblogs.com/yanyiming10243247/p/9294160.html
定义:
- 如果有向图G的任意两个顶点都互相可达,则称图 G是强连通图,如果有向图G存在两顶点u和v使得u不能到v,或者v不能到u,则称图G是强非连通图。
- 如果有向图G不是强连通图,他的子图G2是强连通图,点v属于G2,任意包含v的强连通子图也是G2的子图,则乘G2是有向图G的极大强连通子图,也称强连通分量。
例图:三个分量
dfn[ ]:就是一个时间戳(被搜到的次序),一旦某个点被DFS到后,这个时间戳就不再改变(且每个点只有唯一的时间戳)。所以常根据dfn的值来判断是否需要进行进一步的深搜。
low[ ]:该子树中,且仍在栈中的最小时间戳,像是确立了一个关系,low[ ]相等的点在同一强连通分量中。
注意初始化时 dfn[ ] = low[ ] = ++cnt.
个人理解是:2个及以上节点强连通分量一定有环。
tarjan算法:
基本:求强连通分量个数
low[x]数组存的是与x相连接的顶点low的最小值(最开始被初始化为dfn),当前节点能够通过DFS遍历访问到的最小的节点的访问次序
如果low[x]==dfn[x]表明搜到头了,强连通分量个数就++,
原因:能够遍历其属强连通分量的点的起始点,而且没有其他点属于其他强连通分量能够有一条有向路径连到这个节点来的节点。
在Tarjan算法中,除了使用一个数组来记录节点的访问顺序,还会用到一个称为"low"数组。"low"数组用来记录当前节点能够通过DFS遍历访问到的最小的节点的访问次序。
在进行DFS遍历的过程中,当遍历到某个节点时,会递归地访问其邻居节点。在访问邻居节点的过程中,会不断更新当前节点的"low"值,确保它记录的是能够通过DFS访问到的最小的节点的访问次序。
具体来说,"low"数组的更新规则如下:
- 如果当前节点u是首次被访问,将其"low"值设置为其访问次序。
- 对于当前节点u的每个邻居节点v,如果v尚未被访问过(即v的访问次序为0),则将v递归地进行DFS遍历,并在遍历过程中更新u的"low"值为min(low[u], low[v])。
- 如果邻居节点v已经被访问过,并且v不在当前DFS搜索树中(即v不是u的父节点),则将u的"low"值更新为min(low[u], dfn[v]),其中dfn[v]表示节点v的访问次序。
通过这样的更新规则,可以确保"low"数组记录了当前节点能够通过DFS访问到的最小的节点的访问次序。这个"low"值在Tarjan算法中用于判断节点是否是强连通分量的根节点。
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+9;
int dfn[N];
int low[N];
int head[N];
int vis[N];
int t=1;
int jsq=0;
struct node
{
int t,net;
} e[10*N];
int tot=1;
void add(int x,int y)
{
e[tot].t=y;
e[tot].net=head[x];
head[x]=tot++;
}
int tar(int x)
{
dfn[x]=low[x]=++t;
vis[x]=1;
for(int i=head[x]; i; i=e[i].net)
{
int b=e[i].t;
if(vis[b]==0)//没搜过
{
tar(b);
}
if(vis[b]==1)//搜过
{
low[x]=min(low[x],low[b]);
}
}
if(low[x]==dfn[x])
{
jsq++;
}
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
int u,v;
while(m--)
{
scanf("%d%d",&u,&v);
add(u,v);
}
for(int i=1; i<=n; i++)
{
if(!dfn[i])//没有搜过
{
tar(i);
}
}
printf("%d\n",jsq);
return 0;
}
应用:
2. 缩点
例题:https://www.luogu.com.cn/problem/P3387
这题是想把有环图转换成无环图然后再通过拓扑+dp求最长路
转换无环图的方法就是缩点 缩点就是把一个强连通分量当成一个点(强连通分量一定有环 无法用拓扑求最长路)
AC代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+9;
int dfn[N],p[N];//p存储缩点之后的点的点的权值
int low[N];
int head[N],headq[N];//两张图的
int vis[N];
int a[N];
int sta[N];//栈
int r[N];//入度
int num=0;
int t=1;
int m;
int jsq=0;
int dp[N];//路径
int color[N];//对同一个强连通分量染成一样的颜色
struct node
{
int t,net,f;
} e[10*N],eq[10*N];
int tot=1;
void add(int x,int y)
{
e[tot].f=x;//每条边的起点也得存储
e[tot].t=y;
e[tot].net=head[x];
head[x]=tot++;
}
int totq=1;
void addq(int x,int y)
{
eq[totq].f=x;
eq[totq].t=y;
eq[totq].net=headq[x];
headq[x]=totq++;
}
void tar(int x)
{
dfn[x]=low[x]=++t;//初始化
sta[++num]=x;//一个强连通分量的所有点都入栈,最后染色的时候再出栈
vis[x]=1;
for(int i=head[x]; i; i=e[i].net)
{
int b=e[i].t;
if(vis[b]==0)
{
tar(b);
}
if(vis[b]==1)//搜过
{
low[x]=min(low[x],low[b]);
}
}
if(low[x]==dfn[x])
{
jsq++;//计数器
do
{
low[sta[num]]=jsq;
color[sta[num]]=jsq;//染色
vis[sta[num]]=2;//等于一个离谱的数 下次就不会再搜了
p[jsq]+=a[sta[num]];//一个点的权要加起来
}
while(sta[num--]!=x);//最初的起点
}
}
queue<int>qu;
void bfs()//拓扑
{
while(!qu.empty())
{
int x=qu.front();
qu.pop();
for(int i=headq[x];i;i=eq[i].net)
{
int b=eq[i].t;
r[b]--;
dp[b]=max(dp[b],dp[x]+p[b]);
if(r[b]==0)
qu.push(b);
}
}
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
}
int u,v;
while(m--)
{
scanf("%d%d",&u,&v);
add(u,v);
}
for(int i=1; i<=n; i++)
{
if(!dfn[i])
{
tar(i);
}
}
for(int i=1;i<tot;i++)//这一步是想建立一个新的图
{
int xx=color[e[i].f];//找出每个边起点和终点分别对应的缩点编号 如果有边就建边
int yy=color[e[i].t];
if(xx!=yy)
{
r[yy]++;//入度
addq(xx,yy);
}
}
for(int i=1;i<=jsq;i++)//jsq为缩点编号
{
if(r[i]==0)
{
qu.push(i);
dp[i]=p[i];
}
}
bfs();///我会告诉你我wa在这里忘了写吗
int maxx=-1;
for(int i=1;i<=jsq;i++)
{
maxx=max(dp[i],maxx);
}
printf("%d\n",maxx);
return 0;
}
3. 割点
(把这个点去掉 图就不再连通)
例题:https://www.luogu.com.cn/problem/P3388
题目是无向图的。
对于根节点:计算其子树数量,如果有2棵即以上的子树,就是割点。因为如果去掉这个点,这两棵子树就不能互相到达。
如果不是:就通过
https://www.luogu.com.cn/article/6jm9juwe
在求解割点(关节点)的问题中,low[u]表示从节点u出发通过非父子关系边能够到达的最早的节点的访问次序。因此,当我们在处理节点u的子节点v时,更新low[u]为min(low[u], low[v])的目的是为了找到u能够通过DFS访问到的最小的节点的访问次序。这里的父子是以dfs
而在求解强连通分量的问题中,low[u]表示从节点u出发通过DFS遍历访问到的最小的节点的访问次序。因此,low[u]的更新规则会略有不同,通常是通过比较low[u]和dfn[v]来更新的,以确保节点u的low值正确地记录了子树能够回溯到的最早的节点的访问次序。
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=2e4+9;
int head[N];
int tot=1;
int vis[N],dfn[N],low[N];
struct node
{
int t,net;
}e[10*N];
int t=0;
void add(int x,int y)
{
e[tot].t=y;
e[tot].net=head[x];
head[x]=tot++;
}
void tar(int u,int fa)
{
dfn[u]=low[u]=++t;
int sum=0;
for(int i=head[u];i;i=e[i].net)
{
int b=e[i].t;
if(!dfn[b])
{
tar(b,fa);
low[u]=min(low[u],low[b]);
if(low[b]>=dfn[u]&&u!=fa)//表示儿子𝑣点回溯可到达的最先点的时间戳大于等于𝑢的时间戳,证明其不能通过𝑢以外的点与已遍历点相连
vis[u]=1;
if(u==fa)//能回溯回来两次,说明有2个以上子树,不然遍历其中一个已经遍历完了
sum++;
}
low[u]=min(low[u],dfn[b]);
}
if(sum>=2&&u==fa)//判断根
vis[u]=1;
}
int main()
{
int ans=0;
int n,m;
scanf("%d%d",&n,&m);
int u,v;
while(m--)
{
scanf("%d%d",&u,&v);
add(u,v);
add(v,u);
}
for(int i=1;i<=n;i++)
{
if(dfn[i]==0)
tar(i,i);
}
for(int i=1;i<=n;i++)
{
if(vis[i])
ans++;
}
printf("%d\n",ans);
for(int i=1;i<=n;i++)
{
if(vis[i])
printf("%d ",i);
}
printf("\n");
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现