tarjan全家桶

首先是tarjan家族的祖传操作:
维护两个数组:

  • dfn:进入该点的时间戳
  • low:能追溯到的最小时间

通用板子(不同用途会有细微不同):

inl void tarjan(int x){
    dfn[x]=low[x]=++dfs_clock;
    for(int i=head[x];i;i=nxt[i]){
        int y=to[i];
        if(!dfn[y]){
            tarjan(y);
            low[x]=min(low[x],low[y]);
        }else low[x]=min(low[x],dfn[y]);
    }
}

缩点

每次找到一个新的点 我们把他压入栈中

对于一个点 如果它的 \(low\) 数组小于 \(dfn\) 那么它一定可以到达dfs搜索树上方某点 那么加上上方的点答案会更大

相反 如果 \(dfn=low\) 那么它和下方能到达它的点合起来答案最大 那么弹出栈直到弹出当前点 \(x\) 这些点就是一个强联通分量

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long 
#define inl inline
#define mid (l+r>>1)
const int N=1e5+5;
const int M=1e5+5;
const int mod=1e5+3;
inl int read(){
    int x=0,f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
    return x*f;
}
inl void write(int x){
    if(x<0){x=-x;putchar('-');}
    if(x>9)write(x/10);
    putchar(x%10+'0');
}
inl void writel(int x){write(x);putchar('\n');}
int n,m,a[N],id,idx[N],sum[N],u[N],v[N],f[N],ans;
int dfn[N],low[N],vis[N],st[N],top,dfs_clock;
int head[N],nxt[N],to[N],cnt;
inl void add(int u,int v){
    nxt[++cnt]=head[u];
    to[cnt]=v;
    head[u]=cnt;
}
inl void tarjan(int x){
    dfn[x]=low[x]=++dfs_clock;
    st[++top]=x;vis[x]=1;
    for(int i=head[x];i;i=nxt[i]){
        int y=to[i];
        if(!dfn[y]){
            tarjan(y);
            low[x]=min(low[x],low[y]);
        }else if(vis[y]){
            low[x]=min(low[x],dfn[y]);
        }
    }
    if(dfn[x]^low[x])return;id++;
    while(1){
        int y=st[top--];vis[y]=0;
        idx[y]=id;sum[id]+=a[y];
        if(x==y)break;
    }
}
signed main(){
    n=read();m=read();
    for(int i=1;i<=n;i++)a[i]=read();
    for(int i=1;i<=m;i++){
        u[i]=read(),v[i]=read();
        add(u[i],v[i]);
    }
    for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i);
    memset(head,0,sizeof head);
    memset(nxt,0,sizeof nxt);
    cnt=0;
    for(int i=1;i<=m;i++){
        if(idx[u[i]]==idx[v[i]])continue;
        add(idx[u[i]],idx[v[i]]);
    }
    for(int i=1;i<=id;i++)f[i]=sum[i];
    for(int x=id;x;x--){
        for(int i=head[x];i;i=nxt[i]){
            int y=to[i];
            f[y]=max(f[y],f[x]+sum[y]);
        }
    }
    for(int i=1;i<=id;i++)ans=max(ans,f[i]);
    writel(ans);
    return 0;
}

割点

一个点是割点 当且仅当:

  • 它是根&&儿子数 \(\ge 2\)
  • 它不是根&& \(low_y>=dfn_x\)

对于第一条 如果它是根且有两儿子以上 那么这些儿子只能从根互相联通 所以根是割点

对于第二条 如果它不是根&& \(low[y]>=dfn[x]\) 意味着儿子 \(y\) 无法到达 \(x\) 上面或之前的点 那么 \(x\) 一定是割点

这样说的前提条件:

else low[x]=min(low[x],dfn[y]);

如果这样写:

else low[x]=min(low[x],low[y]);

\(y\) 可能会取到 \(low_x\) 我们要判 \(y\) 能否到达 \(x\) 上面或之前的点 那么答案一定不对

(对于缩点 上面写法是对的 但点双边双一定会错 建议都写上边的一种)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long 
#define inl inline
#define mid (l+r>>1)
const int N=2e5+5;
const int M=1e5+5;
const int mod=1e5+3;
inl int read(){
    int x=0,f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
    return x*f;
}
inl void write(int x){
    if(x<0){x=-x;putchar('-');}
    if(x>9)write(x/10);
    putchar(x%10+'0');
}
inl void writei(int x){write(x);putchar(' ');}
inl void writel(int x){write(x);putchar('\n');}
int n,m,a[N],u,v,ans[N],num;
int dfn[N],low[N],dfs_clock;
int head[N],nxt[N],to[N],cnt;
inl void add(int u,int v){
    nxt[++cnt]=head[u];
    to[cnt]=v;
    head[u]=cnt;
}
inl void tarjan(int x,int flag){
    dfn[x]=low[x]=++dfs_clock;
    int child=0;
    for(int i=head[x];i;i=nxt[i]){
        int y=to[i];
        if(!dfn[y]){
            tarjan(y,0);child++;
            if(flag&&child>=2)ans[x]=1;
            if(!flag&&low[y]>=dfn[x])ans[x]=1;
            low[x]=min(low[x],low[y]);
        }else low[x]=min(low[x],dfn[y]);
    }
}
signed main(){
    n=read();m=read();
    for(int i=1;i<=m;i++){
        u=read(),v=read();
        add(u,v);add(v,u);
    }
    for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i,1);
    for(int i=1;i<=n;i++)if(ans[i])num++;
    writel(num);
    for(int i=1;i<=n;i++)if(ans[i])writei(i);
    return 0;
}

点双

点双连通:若对于一个无向图,其任意一个节点对于这个图本身而言都不是割点,则称其点双连通。也就是说,删除任意点及其相关边后,整个图仍然属于一个连通分量。

点双连通分量:无向图中,极大的点双连通子图。与连通分量类似,抽离出一些点及它们之间的边,使得抽离出的图是一个点双连通图,在这个前提下,使得抽离出的图越大越好。

根据我们会发现:两个点双最多只有一个公共点(即都有边与之相连的点);且这个点在这两个点双和它形成的子图中是割点。

那么我们还是用栈存 找到割点/树根开始弹栈 但由于一个割点/树根可能属于多个点双 我们可以弹到它的儿子 然后单独把割点加上

弹出来的就是一个点双 注意要特判自环和独立点(无边相连的点)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long 
#define inl inline
const int N=4e6+5;
const int M=1e5+5;
const int mod=1e5+3;
inl int read(){
    int x=0,f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
    return x*f;
}
inl void write(int x){
    if(x<0){x=-x;putchar('-');}
    if(x>9)write(x/10);
    putchar(x%10+'0');
}
inl void writei(int x){write(x);putchar(' ');}
inl void writel(int x){write(x);putchar('\n');}
int n,m,a[N],u,v,num,id,st[N],top;
int dfn[N],low[N],dfs_clock;
int head[N],nxt[N],to[N],cnt;
vector<int>ve[N];
inl void add(int u,int v){
    nxt[++cnt]=head[u];
    to[cnt]=v;
    head[u]=cnt;
}
inl void tarjan(int x,int flag){
    dfn[x]=low[x]=++dfs_clock;
    st[++top]=x;
    if(flag&&!head[x])return ve[++num].push_back(x),void();
    for(int i=head[x];i;i=nxt[i]){
        int y=to[i];
        if(!dfn[y]){
            tarjan(y,0);
            if(low[y]>=dfn[x]){
                num++;
                while(1){
                    int p=st[top--];
                    ve[num].push_back(p);
                    if(p==y)break;
                }
                ve[num].push_back(x);
            }
            low[x]=min(low[x],low[y]);
        }else low[x]=min(low[x],dfn[y]);
    }
}
signed main(){
    n=read();m=read();
    for(int i=1;i<=m;i++){
        u=read(),v=read();
        if(u==v)continue;
        add(u,v);add(v,u);
    }
    for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i,1);
    writel(num);
    for(int i=1;i<=num;i++){
        writei(ve[i].size());
        for(auto x:ve[i])writei(x);puts("");
    }
    return 0;
}

边双

还是先看割边

一条边是割边 当且仅当 $$low_y>dfn_x$$

很好理解 如果 \(low_y<dfn_x\) 那么这两个点只有这一条边相连 割掉这条边两个点就必然不联通 否则证明 \(y\) 有另一条边指向 \(x\) 或之前的点

边双定义与点双类似 但由于割边必然不属于任意一个边双 那么我们可以一遍tarjan求割边 再dfs一遍即可

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long 
#define inl inline
const int N=4e6+5;
const int M=1e5+5;
const int mod=1e5+3;
inl int read(){
    int x=0,f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9'){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
    return x*f;
}
inl void write(int x){
    if(x<0){x=-x;putchar('-');}
    if(x>9)write(x/10);
    putchar(x%10+'0');
}
inl void writei(int x){write(x);putchar(' ');}
inl void writel(int x){write(x);putchar('\n');}
int n,m,a[N],u,v,num,id,st[N],top,gb[N],vis[N];
int dfn[N],low[N],dfs_clock;
int head[N],nxt[N],to[N],cnt=1;
vector<int>ve[N];
inl void add(int u,int v){
    nxt[++cnt]=head[u];
    to[cnt]=v;
    head[u]=cnt;
}
inl void tarjan(int x,int fa){
    dfn[x]=low[x]=++dfs_clock;
    for(int i=head[x];i;i=nxt[i]){
        int y=to[i];
        if(!dfn[y]){
            tarjan(y,x);
            if(low[y]>dfn[x])gb[i]=gb[i^1]=1;
            low[x]=min(low[x],low[y]);
        }else if(y^fa)low[x]=min(low[x],dfn[y]);
    }
}
inl void dfs(int x,int num){
    vis[x]=1;
    ve[num].push_back(x);
    for(int i=head[x];i;i=nxt[i]){
        int y=to[i];
        if(vis[y]||gb[i])continue;
        dfs(y,num);
    }
}
signed main(){
    n=read();m=read();
    for(int i=1;i<=m;i++){
        u=read(),v=read();
        if(u==v)continue;
        add(u,v);add(v,u);
    }
    for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i,0);
    for(int i=1;i<=n;i++)if(!vis[i])dfs(i,++num);
    writel(num);
    for(int i=1;i<=num;i++){
        writei(ve[i].size());
        for(auto x:ve[i])writei(x);puts("");
    }
    return 0;
}
posted @ 2023-11-02 09:59  xiang_xiang  阅读(28)  评论(0)    收藏  举报