题解 P1726 【上白泽慧音】

说在前面:既然是没什么包装的模板题,那么建议大家打细致一些,如果不很清楚,代码量多一些也没关系,一条一条重在理解透彻


好,进入正题:

很明显的一道Tarjan,题解中讲Tarjan的神犇也有很多,但是我自认为还可以多用些图和Markdown将这个算法讲的更清楚些,所以这里重新讲一下Tarjan:


这里普及基本概念:

  • 强连通分量:对于图G来说的一个子图中,任意两个点都可以彼此到达,这个子图就被称为图G的连通分量(一个点就是最小的连通分量)
  • 时间戳:搜索到一个点时,这个点将被赋予一个 唯一 的时间量,并且越早搜到的点时间戳越小(当然了)

Tarjan是一个基于深搜,可以求解图中强连通分量的的算法,基本思想请看图:

在我们对一个图进行深度优先搜索时,走过的边会得到一棵搜索树,搜索树可以看做是原图抛弃了一些边而形成的

这便是一棵搜索树了

我们对抛弃掉的边做一个分类,对于这棵树中被还原的原图中的边有三种:

  • 红色的边-横叉边
  • 蓝色的边-前向边
  • 绿色的边-后向边

不难发现,横叉边和前向边都无法构成回路,即不能形成大于一个点的强连通分量,所以我们要做的,就是找出重要的后向边来

怎么找后向边呢?你可以回想一下刚才提到的“时间戳”,没错,如果我们搜索到了一个之前搜过的点,且他当前点的祖先一样,那么毫无疑问的,我们走在了后向边上

Tarjan算法使用两个数组来维护这个信息:

  • dfn[maxn]:储存每个点的时间戳
  • low[maxn]:储存每个点访问祖先的能力

什么是访问祖先的能力呢?就是说,这个点最多能走回头路到什么地步,low数组储存的是他能访问到的最早祖先的dfn值,如果这个点没有回头路可走,那low值就是他自己的dfn值咯

好了,说了这么多,相信你已经有一些感觉了,我们来看Tarjan的模板代码:

struct edge{
    int x,y,w;
}E[maxm];
int dfn[maxn],low[maxn],tmmk=0;
bool v[maxn];//v数组用来跟踪这个点是否已经处理完毕,我们稍后会见到它的用法
stack<int> S;
void tarjan(int x)
{
   dfn[x]=low[x]=++tmmk;//每个点在最开始被访问时,时间戳和low值都是一样的
   S.push(x);
   v[x]=true;
   for(int i=head[x];i;i=next[i])//链式前向星查邻接点
   {
     int y=E[i].y;//不熟悉的同学请像这样打多一点,有助于理解
     if(!dfn[y])//dfn的初值都是0,如果这个点没有搜过,就递归搜索
     {
       tarjan(y);
       low[x]=min(low[x],low[y]);//此时搜索已经回溯,我们可以确定y的low值已经更正,所以用它来更新x的
     }//因为y在x的后面,所以y能访问到的祖先x一定可以访问到
     else
       if(v[y])//如果y点搜过了且在栈里,说明我们找到了 后!向!边!
          low[x]=min(low[x],dfn[y]);
   }//但是此时我们还不能急着处理,为什么?因为有回溯到根了以后我们在收集到了完整的信息
   if(dfn[x]==low[x])
   {//dfn和low一样,你就是与众不同的根节点
      ans++;//ans是强连通分量的个数
      int y;
      do{
        y=S.top();
        S.pop();
        v[y]=false;
        col[y]=ans;//y点属于编号为ans的强连通分量
        g[ans].push_back(y);//存下每个强连通分量的成员
      }while(y!=x)//在这里将栈里的点全部倒出来(倒垃圾一样..)
   }
}

回到题目!

题目要求求出图中最大强连通分量,和其中所有的点

我们可以看出,以上模板中col数组用不着了,存成员还是必要的,但是要求字典序输出,这个向量就不很方便了,我们可以用优先队列解决这个问题

关于优先队列的基本用法可以看我博客,这里关系不很大不做深究qwq

AC代码:不开O2 55ms(偷懒用stl的代价)开O2 35ms

#include<bits/stdc++.h>
using namespace std;

const int maxn=6000,maxm=110000;
int n,m;

int read(){
    int x=0;char c=getchar();
    while(c<'0' || c>'9')c=getchar();
    while(c>='0' && c<='9'){
        x=(x<<3)+(x<<1)+(c^'0');
        c=getchar();
    }
    return x;
}

int head[maxn],nxt[maxm],id=0;
struct edge{
    int x,y;
}G[maxm];
void add(int x,int y){
    G[++id].x=x;
    G[id].y=y;
    nxt[id]=head[x];
    head[x]=id;
}

struct pointer{//这是一个指针,p指向连通集编号,siz是他的大小方便排序
    int p,siz=0;
    int diction;//diction是第一个点的编号,如果严格字典序请改成数组
}p[maxn];

int dfn[maxn],low[maxn],col[maxn],
    tmmk=0,cnt=0;
stack<int> S;
priority_queue<int,vector<int>,greater<int> > g[maxn];
bool instk[maxn];
void tarjan(int x){
    dfn[x]=low[x]=++tmmk;
    S.push(x);
    instk[x]=true;
    for(int i=head[x];i;i=nxt[i]){
        int y=G[i].y;
        if(!dfn[y]){
            tarjan(y);
            low[x]=min(low[x],low[y]);
        }
        else if(instk[y])
            low[x]=min(low[x],dfn[y]);
    }
    if(dfn[x]==low[x]){
        cnt++;
        int y;
        p[cnt].diction=S.top();
        do{
            y=S.top();
            S.pop();
            instk[y]=false;
            col[y]=cnt;
            p[cnt].p=cnt;
            p[cnt].siz++;
            g[cnt].push(y);
        }while(y!=x);
    }
}

bool cmp(pointer a,pointer b){
    if(a.siz!=b.siz)
        return a.siz>b.siz;
    else
        return a.diction<b.diction;
}

int main(){
    n=read(),m=read();

    int x,y,f;
    for(int i=1;i<=m;i++){
        x=read(),y=read(),f=read();
        add(x,y);
        if(f==2)
            add(y,x);
    }

    memset(dfn,0,sizeof(dfn));
    memset(instk,false,sizeof(instk));
    for(int i=1;i<=n;i++)
        if(!dfn[i])
            tarjan(i);

    sort(p+1,p+cnt+1,cmp);

    printf("%d\n",p[1].siz);
    while(!g[p[1].p].empty()){
        printf("%d ",g[p[1].p].top());
        g[p[1].p].pop();
    }

    return 0;
}

PS:本题数据略水,题解中有些并没有考虑到一般字典序还A掉的,希望同学们注意。另:我这样做也只考虑第一个数的字典序,如果严格排列把字典diction改成数组即可

PSPS:大半夜写这个挺累的,如果有帮到你,请不要吝惜你的赞(笑)

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注