手工栈替代品——非递归dfsの板子

话说……

你说SDOI题毒瘤就毒瘤叭,还用的是过时的 Windows+Cena。。。
你说用Windows就用了叭,还连一个开栈语句都不给写连个O2都不给开。。
生怕选手做出题来系列
SD的小辣鸡ATP已经在SDOI2016的时候被破烂系统栈坑过一回了。。好不容易写个部分分结果爆了栈一分没有
其实ATP一开始是被安利到手工栈的但是不知道为什么当时就是不想学然后正好那一段时间在写网络流然后突然就有了根据非递归的ISAP照葫芦画瓢搞一个非递归dfs的主意。。。
于是下面的板子就新鲜出炉辣!

基于ISAP的非递归dfs

怎么样这个名字听起来是不是很高(zhuang)大(bi)上(逃

ISAP是怎么写的来着?

int ISAP(){
    int u=S;
    bool flag=false;
    //赋初值啦BFS啦之类的过程 
    while (d[S]//如果走到了汇点就更新流量
        flag=false;
        for (int i=cur[u];i!=-1;i=e[i].nxt){
            /*更新当前弧*/
            if (d[u]==d[v]+1&&e[i].flw>0){
            /*记录找到的可以走的点同时标记flag表示找到可行路径*/
            }
        }
        if (flag==false){/*回溯过程*/}
    }
    return Flow;
}

可以看出来它能够以非递归替代递归的关键就是这么几点:

  • 设置了一个当前变量u指向当前操作的节点
  • 设置了一个标记flag表示当前节点还有没有可以扩展的道路
  • 设置了当前弧数组记录当前节点走到哪个位置,回溯回来的时候直接从上次搜索结束的位置开始下一步搜索
  • 有记录路径的数组方便找不到路径时回溯

那么在一般常用的树链剖分啦dfs序啦这些东西里面是不是也可以设置类似的东西呢?显然在树上的话每次回溯是蹦到当前节点的father的,那么只需要对每个节点记录father就可以实现回溯了。剩下的都是和ISAP差不多的啦。

首先贴一个树链剖分两次dfs预处理好像还附带着dfs序的板子:


void dfs(){
    int u=1;
    bool flag;
    while (true){
        flag=false;
        if (deep[u]==0){
            deep[u]=deep[fa[u]]+1;
            size[u]=1;son[u]=0;cur[u]=p[u];
        }
        for (int i=cur[u];i!=0;i=next[i]){
            cur[u]=next[i];
            if (a[i]!=fa[u]){
                fa[a[i]]=u;u=a[i];
                flag=true;break;
            }
        }
        if (flag==false)
          if (u==1) break;
          else{
              int v=fa[u];
              size[v]+=size[u];
              if (size[son[v]]void dfs_again(){
    int u=1,tp=1;
    bool flag;
    while (true){
        flag=false;
        if (top[u]==0){
            top[u]=tp;w[u]=++cnt;
            num[cnt]=v[u];cur[u]=p[u];
            if (son[u]!=0){u=son[u];continue;}
        }
        for (int i=cur[u];i!=0;i=next[i]){
            cur[u]=next[i];
            if (a[i]!=fa[u]&&a[i]!=son[u]){
                u=a[i];tp=a[i];flag=true;break;
            }
        }
        if (flag==false){
            out[u]=cnt;
            if (u==1) break;
            else u=fa[u];
        }
    }
}

跟ISAP长得好像啊有没有!

它的基本思路就是把所有操作分成几类:第一次遇到它的时候要做的操作,枚举子树的时候要做的操作,子树回溯之前要做的操作,当前节点全部枚举完以后要做的操作。这四类操作在普通dfs中的位置如下:

void dfs(int u){
    /*第一次遇到它要做的操作在这里
    比如dfs序入栈时间戳的更新,倍增LCA中father的更新
    还有子树大小size赋初值,deep的更新等等*/
    for (int i=p[u];i!=0;i=nxt[i])
      if (a[i]!=fa[u]){
          /*枚举到子树的时候要做的操作在这里
          比如记录father的数组的更新*/
          dfs(a[i]);
          /*子树回溯的时候要做的操作在这里
          比如用子树size更新当前size,重儿子的更新等等*/
      }
    /*枚举完当前节点以后要做的操作在这里
    比如dfs序中出栈时间戳的更新*/
}

对于第一类操作,它是在第一次遇到这个节点的时候就要做的,所以必须有一个判断依据来告诉我们这个节点是不是第一次被访问到。实际上不需要新开一个vis数组之类的,因为在第一类操作中会有很多被赋值的数组,比如deep啊,top啊,dfs序入栈时间戳啊等等,通过这些数组是否被赋值就可以判断这个节点是不是第一次被访问了。

然后要枚举这个点出发的所有边。用当前弧数组cur来进行枚举。cur的初值是边表的表头,然后每次枚举到一条边的时候要更新一下。如果这条边能走,就说明找到了一个可以枚举的子树,这个时候就要做第二类操作。

如果能够找到可以走的子树,flag会被标记为true。这个时候直接进行下一轮循环来处理当前的新节点就可以了。否则如果flag标记为false,说明这个点出发的所有可行的边已经都被枚举完了,就要进行回溯。这个时候进行第三类操作和第四类操作。

注意第三类操作和第四类操作位置上的区别。第三类操作是子树回溯的时候要做的操作,比如用它的size来更新它父亲的size。这些操作1号节点是不会做的,因为它没有可以更新的father。所以这些操作判断一下当前节点不是1然后再做。

而第四类操作不涉及更新father的问题,1号节点也是有可能做的,所以要在判断flag==false准备回溯的时候接着做。

如果当前需要回溯的节点是1号节点,说明1号节点出发的所有边已经遍历完成,可以退出循环了。否则就回到它的father节点继续循环。

可扩展性

因为ATP写过的题比较少并且在OJ上评测谁闲的没事爆栈玩儿所以这种非递归写法仅用在了树链剖分和倍增LCA的预处理上。。

但是实际上这个东西不仅仅能用到这上面的,因为常用的树形DP啊,Tarjan啊,点分治啊之类的东西它的枚举结构是差不多的,都是每次枚举到一个节点做上面四种基本操作然后再回溯,所以它们都可以改造成这种非递归形式的。

比如。。。。来改造一发Tarjan?虽然ATP也不知道Tarjan会不会爆栈。。不过万一不良心的出题人搞一条链来卡你玩玩怎么办啊对吧= =

原始代码VS改造后代码

void Tarjan(int u){
    w[u]=low[u]=++cnt;
    ext[u]=true;st[++top]=u;
    for (int i=p[u];i!=0;i=nxt[i])
      if (w[a[i]]==0){
          Tarjan(a[i]);
          low[u]=min(low[u],low[a[i]]);
      }else if (ext[a[i]]==true) low[u]=min(low[u],w[a[i]]);
    if (low[u]==w[u]){
        int v;++blk;
        do{
            v=st[top--];ext[v]=false;
            belong[v]=blk;++size[blk];
        }while (v!=u);
    }
}

最明显的区别就是长度了好吧。。。


void Tarjan(int rt){
    int u=rt;
    bool flag=false;
    for (int i=1;i<=n;i++) cur[i]=p[i];
    while (true){
        if (w[u]==0){
            w[u]=low[u]=++cnt;
            ext[u]=true;st[++top]=u;
        }
        flag=false;
        for (int i=cur[u];i!=0;i=nxt[i]){
            cur[u]=i;
            if (w[a[i]]==0){
                fa[a[i]]=u;u=a[i];flag=true;break;
            }else if (ext[a[i]]==true) low[u]=min(low[u],w[a[i]]);
        }
        if (flag==false){
            if (low[u]==w[u]){
                int v;++blk;
                do{
                    v=st[top--];ext[v]=false;
                    belong[v]=blk;++size[blk];
                }while (v!=u);
            }
            if (u==rt) break;
            else{
                low[fa[u]]=min(low[fa[u]],low[u]);
                u=fa[u];
            }
        }
    }
}

所以说这个东西就是一个手工栈的等价替代品啦。只要写出了递归版本的东西,然后划分清楚四种操作,把四种操作对应安放到它应该在的位置就OK啦!

在ATP写这个东西的同时zyf2000也把手工栈的板子发了出来!为了资瓷小伙伴的事业ATP要在这里加一个链接链过去呀!

你可能感兴趣的:(板子们)