剑无情 人却有情

js.js

浅说——tarjan

Tarjan陪伴强连通分量,
生成树完成后思路才闪光。
Euler跑过的七桥古塘
让你,心驰神往……

图论基础知识

相关连接,感谢以下链接(提供部分内容):

https://big-news.cn/2019/02/07/%E5%BC%BA%E8%BF%9E%E9%80%9A%E5%88%86%E9%87%8F/

https://www.langlangago.xyz/index.php/archives/83/

割顶

(特别感谢洛谷,虽然有点小错)

Tarjan是干啥的?

以上摘自百科

你也看不懂,对吧。那看个毛啊!

tarjan说白了三个用:

1.求强连通分量

2.求LCA

3.无向图中,求割点和桥

----------------------------------------------------------------------------------------------------------------------

 强连通分量

1.啥是强连通分量?

百科

这么说吧,强连通是指这个图中每两点都能够互相到达。

强连通分量也就是最大的强连通子图,很easy吧?

如图,有三个强连通分量(1,2,3)&(4)&(5)。

2.怎么求强连通分量?

噫......很明显,通过肉眼可以很直观地看出(1,2,3)是一组强连通分量,但很遗憾,代码并没有眼睛,所以该怎么判断强连通分量呢?

如果仍是上面那张图,我们对它进行dfs遍历。

可以注意到红边非常特别,因为如果按照遍历时间来分类的话,其他边都指向在自己之后被遍历到的点,而红边指向的则是比自己先被遍历到的点。

如果存在这么一条边,那么我们可以yy一下

从一个点出发,一直向下遍历,然后忽得找到一个点,那个点竟然有条指回这一个点的边!

那么想必这个点能够从自身出发再回到自身

想必这个点和其他向下遍历的该路径上的所有点构成了一个环,

想必这个环上的所有点都是强联通的。

但只是强联通啊,我们需要求的可是强连通分量啊......

比如说图中红色强连通分量,而蓝色只是强联通图

因此我们只需要知道这个点u下面的所有子节点有没有连着这个点的祖先就行了。

但似乎还有一个问题啊......

我们怎么知道这个点u它下面的所有子节点一定是都与他强联通的呢?

这似乎是不对的,这个点u之下的所有点不一定都强联通

那么怎么在退回到这个点的时候,知道所有和这个点u构成强连通分量的点呢?

开个栈记录就行了

什么?!这么简单?

没错~就是这么简单~

如果在这个点之后被遍历到的点已经能与其下面的一部分点(也可能就只有他一个点)已经构成强连通分量,即它已经是最大的。

那么把它们一起从栈里弹出来就行了。

所以最后处理到点u时如果u的子孙没有指向其祖先的边,那么它之后的点肯定都已经处理好了,一个常见的思想,可以理解一下。

所以就可以保证栈里留下来u后的点都是能与它构成强连通分量的。

似乎做法已经明了了,用程序应该怎么实现呢?

3.怎么码代码?

首先介绍一下辅助数组

 

(1)、dfn[ ],表示这个点在dfs时是第几个被搜到的。
(2)、low[ ],表示这个点以及其子孙节点连的所有点中dfn最小的值
(3)、stack[ ],表示当前所有可能能构成强连通分量的点。
(4)、vis[ ],表示一个点是否在stack[ ]数组中。

 

那么按照之上的思路,我们来考虑这几个数组的用处以及算法的具体过程。

假设现在开始遍历点u:

  • 首先初始化dfn[u]=low[u]=第几个被dfs到
    dfn可以理解,但为什么low也要这么做呢?
    因为low的定义如上,也就是说如果没有子孙与u的祖先相连的话,dfn[u]一定是它和它的所有子孙中dfn最小的(因为它的所有子孙一定比他后搜到)。

     

  • 将u存入stack[ ]中,并将vis[u]设为true
    stack[ ]有什么用?
    如果u在stack中,u之后的所有点在u被回溯到时u和栈中所有在它之后的点都构成强连通分量。(也就是上文中所说的开个栈记录)

     

  • 遍历u的每一个能到的点,如果这个点dfn[ ]为0,即仍未访问过,那么就对点v进行dfs,然后low[u]=min{low[u],low[v]} low[ ]有什么用?
    应该能看出来吧,就是记录一个点它最大能连通到哪个祖先节点(当然包括自己)
    如果遍历到的这个点已经被遍历到了,那么看它当前有没有在stack[ ]里,如果有那么low[u]=min{low[u],low[v]}
    如果已经被弹掉了,说明无论如何这个点也不能与u构成强连通分量,因为它不能到达u
    如果还在栈里,说明这个点肯定能到达u,同样u能到达他,他俩强联通。

     

  • 假设我们已经dfs完了u的所有的子树,那么之后无论我们再怎么dfs,u点的low值已经不会再变了。 
    那么如果dfn[u]=low[u]这说明了什么呢?
    再结合一下dfn和low的定义来看看吧
    dfn表示u点被dfs到的时间,low表示u和u所有的子树所能到达的点中dfn最小的。
    这说明了u点及u点之下的所有子节点没有边是指向u的祖先的了,即我们之前说的u点与它的子孙节点构成了一个最大的强连通图即强连通分量
    此时我们得到了一个强连通分量,把所有的u点以后压入栈中的点和u点一并弹出,将它们的vis[ ]置为false,如有需要也可以给它们染上相同颜色(后面会用到)

于是tarjan求强连通分量的部分到此结束

代码大概是这样的

void tarjan(int u)
{
    s.push(u);
    dfn[u]=low[u]=++k;   //dfn[u]表示u是第几个(++k)被搜到的(时间戳),low[u]表示u的连接点中最先被搜到的点v,点v是第几个被搜到的就是low[u]的值 (时间戳) 
    for(int i=head[u];i;i=e[i].next) 
    {
        int v=e[i].v;
        if(!dfn[v])  //没有搜过v,就去搜v
        {
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(!vis[v]) //搜过v,但是还在栈里 
        {
            low[u]=min(low[u],dfn[v]);
        }
    }
    int v=-1;
    if(dfn[u]==low[u])
    {
        sum++;
        while(u!=v)  //u==v就跳出了呗 
        {
            num[sum]++;
            v=s.top();
            vis[v]=1;
            s.pop();
        }
    }
}

 来道题练练手P2863 [USACO06JAN]牛的舞会The Cow Prom

题意为:给定一个图,要求图中节点数大于一的强联通分量个数。(模板题)

#include<cstdio> 
#include<string>
#include<stack>
using namespace std;
int read()
{
    int x=0,f=1;char c=getchar();
    while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
    while(isdigit(c)){x=x*10+c-'0';c=getchar();}
    return x*f;
}
const int maxn=10005,maxm=50005;
int n,m;
struct edge{
    int v,next;
}e[maxm*2];
int cnt,head[maxn],k;
int dfn[maxn],low[maxn];
bool vis[maxn];
stack <int> s;
int sum,num[maxn];
int ans;
void add(int u,int v) //建树大家都知道
{
    e[++cnt].v=v;
    e[cnt].next=head[u];
    head[u]=cnt;
}
void readdata()  //呵呵快读,_|^^|●
{
    n=read(),m=read();
    for(int i=1;i<=m;i++)
    {
        int a,b;
        a=read(),b=read();
        add(a,b);
    }
}
void tarjan(int u) //tar……jan
{
     s.push(u);
    dfn[u]=low[u]=++k;   //dfn[u]表示u是第几个(++k)被搜到的(时间戳),low[u]表示u的连接点中最先被搜到的点v,点v是第几个被搜到的就是low[u]的值 (时间戳) 
    for(int i=head[u];i;i=e[i].next) 
    {
        int v=e[i].v;
        if(!dfn[v])  //没有搜过v,就去搜v
        {
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(!vis[v]) //搜过v,但是还在栈里 
        {
            low[u]=min(low[u],dfn[v]);
        }
    }
    int v=-1;
    if(dfn[u]==low[u])
    {
        sum++;
        while(u!=v)  //u==v就跳出了呗 
        {
            num[sum]++;
            v=s.top();
            vis[v]=1;
            s.pop();
        }
    }
}
void out()
{
    printf("%d",ans);
}
void work()
{
    for(int i=1;i<=n;i++)
    {
        if(!dfn[i])
        tarjan(i);
    }
    for(int i=1;i<=sum;i++) //大于1的强连通分量
    {
        if(num[i]>1)
        {
            ans++;
        }
    }
    out(); //shuchu,输出
}
int main()
{
    readdata();
    work();
    return 0;
}

缩点

1.什么时候要用缩点

众所周知,有向无环图总是有着一些蜜汁优越性,因为没有环,你可以放心的在上面跑dfs,搞DP,但如果是一张有向有环图,事情就会变得尴尬起来了

思考一下会发现如果不打vis标记就会t飞(一直在环里绕啊绕),但是如果打了,又不一定能保证最优解

而你一看题目却发现显然根据一些贪心的原则,这个环上每个点的最大贡献都是整个环的总贡献

这个时候缩点就显得很有必要了,因为单个点的贡献和整个环相同,为什么不去把整个环缩成一个超级点呢?

这个环只是为了好理解,事实上他应该是一个强连通分量,显然如果只缩掉一个强连通图,图中仍然有环存在

缩点的一个栗子

2.怎么缩点

一般用染色法(就是把强连通分量的所有点归为一组)

来道题练练手P2341 [HAOI2006]受欢迎的牛

所以,我们再来分析一下这道题。

首先,不难发现,如果这所有的牛都存在同一个强联通分量里。那么它们一定互相受欢迎。

那么,我们怎么来找明星呢。

很简单,找出度为0的强联通分量中的点。这样可以保证所有的人都喜欢它,但是它不喜欢任何人,所以说不存在还有人事明星。

此题还有一个特殊情况:

如果有两个点分别满足出度为零的条件,则没有明星,这样无法满足所有的牛喜欢他。

有了上边的解释,题目就不是那么难了,代码如下

#include<cstdio> 
#include<string>
#include<iostream>
#include<stack>
using namespace std;
const int maxn=10005,maxm=50005;
int n,m;
struct edge{
    int v,next;
}e[maxm];
int head[maxn],cnt;
int dfn[maxn],low[maxn],vis[maxn],index,sum;
int belong[maxn];
int k,temp[maxn],forever[maxn];
int ans[maxn];
int cord[maxn];
int t,key;
stack <int> q;

int read();
void readdata();
int add(int u,int v);
void work();
int tarjan(int u);
int read();
int find(int he);
void out();

int main()
{
    readdata();
    work();
    out();
    return 0;
}

int read() //快读 
{
    int x=0,f=1;char c=getchar();
    while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
    while(isdigit(c)){x=x*10+c-'0';c=getchar();}
    return x*f;
}

void out()  //输出 
{
    for(int i=1;i<=sum;i++)
    {
        //printf("%d\n",forever[i]);
        if(!cord[i])
        {
            t++;
            key=forever[i];
            if(t==2)  //出度数超过一个,则没有明星 
            {
                printf("0");
                return;
            }
        }
    }
    printf("%d",key); //输出明星个数 
}

void readdata()  //读入 
{
    n=read(),m=read();
    for(int i=1;i<=m;i++)
    {
        int a,b;
        a=read(),b=read();
        add(a,b);   
    }
}

int add(int u,int v)   //建树 
{
    e[++cnt].v=v;
    e[cnt].next=head[u];
    head[u]=cnt;
}

void work()  //tarjan预备操作 
{
    for(int i=1;i<=n;i++)
    {
        if(!dfn[i])
        {
            tarjan(i);
        }
    }
}

int tarjan(int u) //tarjan 
{
    q.push(u);
    dfn[u]=low[u]=++index;
    vis[u]=1; 
    for(int i=head[u];i;i=e[i].next)
    {
        int v=e[i].v;
        if(!dfn[v])
        {
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(vis[v])
        {
            low[u]=min(low[u],dfn[v]);
        }
    }
    int v=0; 
    if(dfn[u]==low[u])
    {
        ++sum;
        while(u!=v)
        {
            v=q.top();
            q.pop();
            belong[v]=sum;
            vis[v]=0;
            forever[sum]++;  //记录强连通分量中的结点个数 
            temp[forever[sum]]=v;  //保存该点 
        }
        cord[sum]=find(sum); //出度数 
    }
}

int find(int he) //搜索每个强连通分量的出度数 
{
    int ans=0;
    for(int i=1;i<=forever[he];i++) 
    {
        for(int j=head[temp[i]];j;j=e[j].next) //枚举强连通分量中的每个点的出度数 
        {
            if(belong[e[j].v]!=he)
            {
                ans++;
            }
        }
    }
    return ans;
}

缩点练习2P4742 [Wind Festival]Running In The Sky

自己看题吧……QAQ

思路:tarjan+重新建图+DAGdp

拓扑排序没学过看看吧

#include<cstdio> 
#include<string>
#include<stack>
#include<iostream>
#include<queue>
using namespace std;

const int maxn=200005,maxm=500005;
int n,m;
int k[maxn];
struct edge{
    int v,next;
}e[maxm];
struct edge2{
    int v,next;
}edag[maxm];
int head[maxn],cnt;
int dfn[maxn],low[maxn],vis[maxn];
int index;
int sum;
int maxk[maxn],sumk[maxn],belong[maxn],forever[maxn],temp[maxn];
int degin[maxn];
stack <int> s;
int head2[maxn];
int f[maxn][2];
queue <int> q;

void readdata();                //读入 
int read();                     //快读 
void work();                    //计算&调用程序 
void addedge(int u,int v);      //建树 
void tarjan(int u);             //tarjan缩点 
void findin();                  //dp的预处理 
void adddag(int u,int v);       //缩点后新建树 
void topsort();                 //拓扑排序dp 
void out();                     //输出 

int main()    //主函数 
{
    readdata();
    work();
    topsort();
    out();
    return 0;
}

int read()    
{
    int x=0,f=1;char c=getchar();
    while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
    while(isdigit(c)){x=x*10+c-'0';c=getchar();}
    return x*f;
}

void readdata()
{
    n=read(),m=read();
    for(int i=1;i<=n;i++)
        k[i]=read();
    for(int i=1;i<=m;i++)
    {
        int a,b;
        a=read(),b=read();
        addedge(a,b);
    }
    cnt=0;       //比较懒,后面要用,懒得申明新的变量,直接刷0 QAQ 
}

void addedge(int u,int v)
{
    e[++cnt].v=v;
    e[cnt].next=head[u];
    head[u]=cnt;
}

void work()
{
    for(int i=1;i<=n;i++)  //tarjan正常操作 
    {
        if(!dfn[i])
        tarjan(i);
    }
    findin();
}

void tarjan(int u)
{
    s.push(u);
    dfn[u]=low[u]=++index;
    vis[u]=1;
    for(int i=head[u];i;i=e[i].next)
    {
        int v=e[i].v;
        if(!dfn[v])
        {
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(vis[v])
        {
            low[u]=min(low[u],dfn[v]);
        }
    }
    if(dfn[u]==low[u])
    {
        ++sum;
        int v=0;
        while(u!=v)
        {
            v=s.top();
            s.pop(); 
            vis[v]=0;
            sumk[sum]+=k[v];                  //缩点后该点的总值 
            maxk[sum]=max(maxk[sum],k[v]);    //缩点后该点的最大值 
            belong[v]=sum;                    //记录该点的颜色 
            temp[++forever[sum]]=v;           //temp记录该颜色的一些点 
        }
    }
}

void findin()
{
    for(int i=1;i<=n;i++)
    {
        for(int j=head[i];j;j=e[j].next)
        {
            if(belong[e[j].v]!=belong[i])    //不在同一个强连通分量中 
            {
                adddag(belong[i],belong[e[j].v]);  //新建边 
                degin[belong[e[j].v]]++;          //改颜色入读数++ 
            }
        }
    }
}

void adddag(int u,int v)
{
    edag[++cnt].v=v;
    edag[cnt].next=head2[u];
    head2[u]=cnt;
}

void topsort()  //DAGdp 
{
    for(int i=1;i<=sum;i++)  //先把入读为零的加了 
    {
        if(!degin[i])
        {
            q.push(i);
        }
    }
    while(!q.empty())
    {
        int u=q.front();
        q.pop();
        f[u][0]+=sumk[u];              //f[u][0]是到该点的最大总值 
        f[u][1]=max(f[u][1],maxk[u]);  //f[u][1]是到该点的最大总值中的最大值 
        for(int i=head2[u];i;i=edag[i].next)
        {
            int v=edag[i].v;
            degin[v]--;
            if(!degin[v])
            {
                q.push(v);
            }
            if(f[u][0]>f[v][0])   //自己想吧 
            {
                f[v][0]=f[u][0]; //至于这里为什么不把当前值算上,因为这里加的不一定是最大的,要没有了入度就会在while里会算上。 
                f[v][1]=f[u][1];
            }
            else if(f[u][0]==f[v][0]) 
            {
                f[v][1]=max(f[v][1],f[u][1]);
            }
        }
    }
}

void out()
{
    int ans=-1,maxx=-1;
    for(int i=1;i<=sum;i++)
    {
        if(f[i][0]>ans) //同理 
        {
            ans=f[i][0];
            maxx=f[i][1];
        }
        else if(f[i][0]==ans)
        {
            maxx=max(f[i][1],maxx);
        }
    }
    printf("%d %d",ans,maxx);
}

割顶

百科

也就是:割顶是去掉以后让图不连通的点。

思路

首先选定一个根节点,从该根节点开始遍历整个图(使用DFS)。

对于根节点,判断是不是割点很简单——计算其子树数量,如果有2棵即以上的子树,就是割点。因为如果去掉这个点,这两棵子树就不能互相到达。

通过非父子边(回边),能够回溯到的最早的点(dfn最小)的dfn值(但不能通过连接u与其父节点的边)。对于边(u, v),如果low[v]>=dfn[u],此时u就是割点。

OK?

P3388 【模板】割点(割顶)

#include<cstdio> 
#include<string>
#include<iostream>
using namespace std;

const int maxn=20005,maxm=100005;
int n,m;
struct edge{
    int v,next;
}e[maxm*2];
int head[maxn],cnt;
int dfn[maxn],low[maxn],index;
bool map[maxn];
int ans;

int read();
void readdata();
void add(int u,int v);
void work();
void tarjan(int u,int fa);
void out();

int main()
{
    readdata();
    work();
    out();
    return 0;
}

int read()
{
    int x=0,f=1;char c=getchar();
    while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
    while(isdigit(c)){x=x*10+c-'0';c=getchar();}
    return x*f;
}

void readdata()
{
    n=read(),m=read();
    for(int i=1;i<=m;i++)
    {
        int a,b;
        a=read(),b=read();
        add(a,b);
        add(b,a);
    }
}

void add(int u,int v)
{
    e[++cnt].v=v;
    e[cnt].next=head[u];
    head[u]=cnt;
}

void work()
{
    for(int i=1;i<=n;i++)
    {
        if(!dfn[i])
        {
            tarjan(i,i);
        }
    }
}

void tarjan(int u,int fa)
{
    dfn[u]=low[u]=++index;
    int child=0;
    for(int i=head[u];i;i=e[i].next)
    {
        int v=e[i].v;
        if(!dfn[v])
        {
            tarjan(v,fa);
            low[u]=min(low[u],low[v]);
            if(u!=fa&&low[v]>=dfn[u])
                map[u]=1;
            if(u==fa)
            {
                child++; 
            }
        }
        low[u]=min(low[u],dfn[v]);
    }
    if(u==fa&&child>=2)
    map[u]=1;
}

void out()
{
    for(int i=1;i<=n;i++)
    {
        if(map[i])
        ans++;
    }
    printf("%d\n",ans);
    for(int i=1;i<=n;i++)
    {
        if(map[i])
        printf("%d ",i);
    }
}

 

posted @ 2019-08-04 22:18  mzyczly  阅读(257)  评论(5编辑  收藏  举报