taj

Posted shenbear

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了taj相关的知识,希望对你有一定的参考价值。

首先,taj是用来干嘛的?

taj主要处理两种问题:

1.找强连通分量

2.找割点

均可在O(n)时间内完成

taj的大体思路就是把一张图改成一棵树,钦定一个点,作为根;

然后我们有两个数组:

dfn[u]:u的时间戳

low[u]:u这一坨东西的最早出现的时间(感性理解)


求强联通分量:

强连通分量:有向图中,如果一坨东西,能够互相到达,就是强连通分量,一个点也是

我们会发现,如果u的low>=dfn(实际上low不会>dfn最多=),那么它一定与上面没有联系,所以我们不用考虑它是否会与上面形成强联通分量

在它下面的东西(v)中,我们考虑:

如果说,v的low没有到达u,也就是v无法回到u,那它肯定会在之前就从栈里弹出

如果说,v的low能到达u的上面,那么u一定可以通过v这一条路到达上面,与上面产生联系,low[u]!=dfn[u]

所以,只要当low[u]=dfn[u]时,将栈中到u为止的元素都合并到一个强联通分量重,肯定是没问题的

void taj(int u)
{
    dfn[u]=low[u]=++tim;
    sta[++top]=u;
    vis[u]=1;
    for(int i=head[u];i;i=p[i].next)
    {
        int v=p[i].to;
        if(!dfn[v])
        {
            taj(v);
            low[u]=min(low[u],low[v]);
        }
        else if(vis[v]) low[u]=min(low[v],low[u]);
    }
    if(dfn[u]==low[u])
    {
        int v;
        while(v=sta[top--])
        {
            rt[v]=u;
            vis[v]=0;
            if(u==v) break;
            a[u]+=a[v];
        }
    }
}

求割点:

割点:在一个无向连通块中,删去一个点,将导致它不连通的点

然后就是怎么求:

技术图片

如图,我们将一张图抽象如上,1表示图的上部,5表示下部,中间由环相连。

我们用dfn[x]表示x被访问到的时间,low[x]表示它所在的那一坨的最小值 ~~(感性理解一下)~~

我们从点1开始dfs,记录每一个点的时间戳,然后去搜所有没有搜过的,如果,他的儿子的最上面的时间戳(low[v])

low[v]>=dfn[x]说明v最早也是在x及之后了,也就意味着x上面的与v只能通过x相连,也就是说,只要x上面有(x不为本组祖先),那么x就是割点**

如果x是祖先呢?

那我们将x找到一个儿子便让x的儿子++,并让儿子搜一遍(保证和此儿子联通的都被搜过),那么下次找到的儿子就是和此儿子不连通的,那么只要把x去掉,他两个儿子就不连通了,所以,x为割点

技术图片

不理解上面的算法结合图看,把①去掉,2,3这两个儿子就不连通了

还有一个问题:大佬都说这里low要这样写:low[x]=min(low[x],dfn[v])

至于为什么不像强联通分量里一样,写low[x]=min(low[x],low[v])呢?

因为强联通里只要在强联通中,怎么连边都可以(我们又不会在强联通中割点),但割点不行,我们不能随便连边,low[x]=min(low[x],low[v])表示所有v都连与最先祖先的上,如下图1,但很明显,3,4,5并没有这一条路,所以如果你这么写,代码会认为有这一条路,从而出错

当然,由图的关系得到的low并不会有假边所以写low[x]=min(low[x],low[v])(不理解看代码)

实际:

技术图片

写low[x]=min(low[x],low[v]):

技术图片

当然,你也可以这么理解:

这主要是防止它无向图咋搜下来的,就咋走回去了

void tag(int x,int zx)
{
    int kid=0;
    dfn[x]=low[x]=++tim;
    for(int i=head[x];i;i=p[i].next)
    {
        int v=p[i].to;
        if(!dfn[v])
        {
            tag(v,zx);
            low[x]=min(low[v],low[x]); //是有真边构成,不用改
            if(low[v]>=dfn[x]&&x!=zx) cut[x]=1;//后面无法通过别的方法连接到前面,是割点
            if(x==zx) kid++;
        }
        low[x]=min(low[x],dfn[v]); //要改的指这个
    }
    if(kid>1&&x==zx) cut[x]=1; //祖先有两个+的孩子,是割点
}
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
int n,m;
struct edge
{
    int next,to;
}p[N];
int head[N],num;
void ad(int x,int y)
{
    p[++num].next=head[x];
    p[num].to=y;
    head[x]=num;
}
int dfn[N],low[N],tim,cut[N];
void tag(int x,int zx)
{
    int kid=0;
    dfn[x]=low[x]=++tim;
    for(int i=head[x];i;i=p[i].next)
    {
        int v=p[i].to;
        if(!dfn[v])
        {
            tag(v,zx);
            low[x]=min(low[v],low[x]);
            if(low[v]>=dfn[x]&&x!=zx) cut[x]=1;
            if(x==zx) kid++;
        }
        low[x]=min(low[x],dfn[v]);
    }
    if(kid>1&&x==zx) cut[x]=1;
}
int ans;
int main()
{
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        ad(x,y);
        ad(y,x);
    }
    for(int i=1;i<=n;i++) if(!dfn[i]) tag(i,i);
    for(int i=1;i<=n;i++) ans+=cut[i];
    printf("%d
",ans);
    for(int i=1;i<=n;i++)
        if(cut[i]) printf("%d ",i);
    return 0;
}

同是,注意:

正确:

    for(int i=head[u];i;i=p[i].next)
    {
        int v=p[i].to;
        if(!dfn[v])
        {
            taj(v,zx);
            low[u]=min(low[u],low[v]);
            if(u==zx) kid++;
            if(low[v]>=dfn[u]&&u!=zx) cut[u]=1;
        }
        else if(vis[v]) low[u]=min(low[u],dfn[v]);
        
    }

错误:

    for(int i=head[u];i;i=p[i].next)
    {
        int v=p[i].to;
        if(!dfn[v])
        {
            taj(v,zx);
            low[u]=min(low[u],low[v]);
            if(u==zx) kid++;
            
        }
        else if(vis[v]) low[u]=min(low[u],dfn[v]);
        if(low[v]>=dfn[u]&&u!=zx) cut[u]=1;
    }

就是这个判断的时候,一定是要

只有直接儿子才可以按if(low[v]>=dfn[u]&&u!=zx) cut[u]=1;判,毕竟人家就是这么走的,但是如果是间接儿子,那么,实际上它应该是从别处走来的,所以这么写是错的


 

看一道题:

lgP3469:https://www.luogu.com.cn/problem/P3469

题目大意,n个点,m条边,(<=1e5),本来每两个点间可以互相到达,现在把点i给封锁,有多少到达关系会无法成立

分析:

如果封锁的点不是割点,那么显然:只有这个点到其他n-1个点和反过来的关系无法做到,所以答案为:2*(n-1)

如果封锁的是割点,那么这张图将被分割为若干块,若一块的大小为s,那么它与外界(n-s)个点的关系无法成立,所以这个联通块的贡献是s*(n-s);当然,被封锁的点也无法与外界联系,所以有(n-1)的贡献;总贡献就是所有连通块的贡献+n-1

这样子,我们就可以得到一个O(n^2)的做法:对于每个割点O(n)扫连通块

#include <bits/stdc++.h>
#define random(a,b) rand()%(b-a+1)+a
using namespace std;
const int N = 1e5+5;
int n,m;
struct edge
{
    int next,to;
}p[10*N];
int head[N],num;
void ad(int x,int y)
{
    p[++num]=edge{head[x],y};
    head[x]=num;
}
int dfn[N],low[N],mx[N],tim,cut[N],sd[N];
void taj(int u,int zx)
{
    int kid=0;
    dfn[u]=low[u]=mx[u]=++tim;
    for(int i=head[u];i;i=p[i].next)
    {
        int v=p[i].to;
        if(!dfn[v])
        {
            taj(v,zx);
            mx[u]=max(mx[u],mx[v]);
            low[u]=min(low[u],low[v]);
            if(low[v]>=low[u]&&u!=zx) 
            {
                cut[u]=1;
                sd[u]=mx[u]-dfn[u];
            }
            if(u==zx) kid++;
        }
        else low[u]=min(low[u],dfn[v]);
    }
    if(u==zx&&kid>1) cut[u]=1; 
}
int s=0;
bool vis[N];
void dfs(int u,int x)
{
    vis[u]=1;
    s++;
    for(int i=head[u];i;i=p[i].next)
    {
        int v=p[i].to;
        if(v==x) continue;
    //    printf("@%d %d
",u,v);
        if(!vis[v]) dfs(v,x);
    }
}
void print(int x)
{
    int ans=n-1;
    memset(vis,0,sizeof(vis));
//    puts("Bear");
//    for(int i=head[7];i;i=p[i].next) printf("%d ",p[i].to);puts("");
    for(int i=1;i<=n;i++)
    {
        if(i==x) continue;
        if(!vis[i]) 
        {
            s=0;
            dfs(i,x);
            ans+=s*(n-s);
//            printf("#%d %d
",i,s);
        }
    }
    printf("%d
",ans);
//    system("pause");
}
int main()
{
//    freopen("1.out","w",stdout);
    srand(time(0));
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        ad(x,y);ad(y,x);
    }
    taj(1,1);
    for(int i=1;i<=n;i++)
    {
        if(!cut[i]) printf("%d
",2*(n-1));
        else print(i);
    }
    return 0;
}

//bear Br2 br2 2br BR2 BR nmdp Bear 

 

然后我们就t了,我们需要对算法进行优化:

我们可以在taj的时候就处理出会分出的联通块大小

我们定义sz[u]为taj搜到u时u子树大小,sm[u]表示u被割后,与根分离的点

我们思考如果找到一个会u被割v会被影响的点v,那么sz[v]就是割下来连通块大小,同时,n-sm[u]就是u被割下来后,根那一边的大小

对于根节点,我们直接按上面O(n^2)的做法,O(n)扫一遍

#include <bits/stdc++.h>
#define random(a,b) rand()%(b-a+1)+a
#define int long long
using namespace std;
const int N = 1e5+5;
int n,m;
struct edge
{
    int next,to;
}p[10*N];
int head[N],num;
void ad(int x,int y)
{
    p[++num]=edge{head[x],y};
    head[x]=num;
}
int sdkm(int x){return x*(n-x);}
int dfn[N],low[N],mx[N],tim,cut[N],sd[N],sz[N],sm[N];
void taj(int u,int zx)
{
    int kid=0,lst;
    sz[u]=sm[u]=1;
    dfn[u]=low[u]=mx[u]=lst=++tim;
    for(int i=head[u];i;i=p[i].next)
    {
        int v=p[i].to;
        if(!dfn[v])
        {
            taj(v,zx);
            low[u]=min(low[u],low[v]);
            sz[u]+=sz[v];
            if(low[v]>=dfn[u]&&u!=zx) 
            {
                mx[u]=max(mx[u],mx[v]);
                cut[u]=1;
                sm[u]+=sz[v];
//                if(u==3) printf("@%d %d
",v,sz[v]);
                sd[u]+=sdkm(sz[v]);
                lst=mx[u];
            }
            if(u==zx) kid++;
        }
        else low[u]=min(low[u],dfn[v]);
//        printf("%d %d %d %d
",u,v,low[u],dfn[v]);
    }
    if(u==zx&&kid>1) cut[u]=1; 
}
int s=0;
bool vis[N];
void dfs(int u,int x)
{
    vis[u]=1;
    s++;
    for(int i=head[u];i;i=p[i].next)
    {
        int v=p[i].to;
        if(v==x) continue;
    //    printf("@%d %d
",u,v);
        if(!vis[v]) dfs(v,x);
    }
}
void print(int x)
{
    int ans=n-1;
    memset(vis,0,sizeof(vis));
//    puts("Bear");
//    for(int i=head[7];i;i=p[i].next) printf("%d ",p[i].to);puts("");
    for(int i=1;i<=n;i++)
    {
        if(i==x) continue;
        if(!vis[i]) 
        {
            s=0;
            dfs(i,x);
            ans+=s*(n-s);
//            printf("#%d %d
",i,s);
        }
    }
    printf("%lld
",ans);
//    system("pause");
}
signed main()
{
//    freopen("1.out","w",stdout);
    srand(time(0));
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        int x,y;
        scanf("%lld%lld",&x,&y);
        ad(x,y);ad(y,x);
    }
    taj(1,1);
    if(cut[1]) print(1);
    else printf("%lld
",2*(n-1));
    for(int i=2;i<=n;i++)
    {
        if(!cut[i]) printf("%lld
",2*(n-1));
        else 
        {
//            printf("#%d %d  ",sd[i],sm[i]);
            printf("%lld
",sd[i]+sdkm(n-sm[i])+n-1);
        }
    }
    return 0;
}

//bear Br2 br2 2br BR2 BR nmdp Bear 

以上是关于taj的主要内容,如果未能解决你的问题,请参考以下文章

微信小程序代码片段

VSCode自定义代码片段——CSS选择器

谷歌浏览器调试jsp 引入代码片段,如何调试代码片段中的js

片段和活动之间的核心区别是啥?哪些代码可以写成片段?

VSCode自定义代码片段——.vue文件的模板

VSCode自定义代码片段6——CSS选择器