序言

不得不承认递归是个神奇且强大的东西

但它却也存在致命的缺陷:递归层数太大会爆栈

另一方面递归开销也大于循环,因面临相关方面(时空受限)问题,于此寻找递归改迭代方法

正文

思路应该很明了?

就是自己模拟一个栈,记录函数运行的信息

栈是先进后出的数据结构,符合递归的要求

本试图在某度上寻找,结果都是无关紧要的尾递归改迭代?

下面就以 SCC|Tarjan 算法为例(其实我只会了这个)

例题汉诺塔

20210406 update

在做程序设计课程实践作业时有一题是用非递归实现汉诺塔

我当时就震惊了,递归改迭代这么高级的技术都能写吗

我并不知正解,也许是栈之类,反正我改迭代了,其实这个比下面的例子简单

自行看代码,懒得写解析了,可以直接看下面的例子

void hanio(int n, int t1 = 1, int t2 = 2, int t3 = 3) {
  if (n < 1) return;
  hanio(n-1, t1, t3, t2);
  cout << "from " << t1 << " to " << t3 << '\n';
  hanio(n-1, t2, t1, t3);
}
void hanio(int sz) {
  stack<int> sn, st1, st2, st3, ss;
  int n, t1, t2, t3, state;
  function<void()> push = [&]() {
    sn.push(n);
    st1.push(t1);
    st2.push(t2);
    st3.push(t3);
    ss.push(state);
  };
  function<void()> top = [&]() {
    n = sn.top();
    t1 = st1.top();
    t2 = st2.top();
    t3 = st3.top();
    state = ss.top();
  };
  function<void()> pop = [&]() {
    sn.pop();
    st1.pop();
    st2.pop();
    st3.pop();
    ss.pop();
  };
  n = sz; t1 = 1; t2 = 2; t3 = 3; state = 0;
  push();
  while (sn.size()) {
    top();
    pop();
    switch (state) {
    case 0:
      if (n < 1) continue;
      state = 1;
      push();
      n -= 1; swap(t2, t3); state = 0;
      push();
    continue;
    case 1:
      cout << "from " << t1 << " to " << t3 << '\n';
      n -= 1; swap(t1, t2); state = 0;
      push();
    }
  }
}

例题Tarjan

[USACO06JAN]The Cow Prom S

题目描述:

有一个 n 个点,m 条边的有向图,请求出这个图点数大于 1 的强联通分量个数。

递归版

接下来将根据这份代码来改成非递归版本

非递归版本

先贴代码

int _dfn, _col, _top;
int dfn[N], low[N], ins[N], col[N], sta[N];

struct Stack_Node {                                         // 1
    int u;
    int edge_info;
    int state;
} tarjan_stack[N];

void tarjan(const int &u) {
    static int stack_top;
    stack_top = 0;                                          // 2
    tarjan_stack[++stack_top] = {u, 0, 0};
    while (stack_top) {
        bool flag = false;                                  // 3
        int &u = tarjan_stack[stack_top].u;                 // 4
        auto &i = tarjan_stack[stack_top].edge_info;
        switch (tarjan_stack[stack_top].state) {
        case 0 :
        tarjan_stack[stack_top].state = 1;                  // 5
        flag = false;
            dfn[u] = low[u] = ++_dfn;
            ins[u] = 1;
            sta[++_top] = u;
            for (; i < (int)e[u].size(); ++i) {
#define v e[u][i]                                           // 6
                if (!dfn[v]) {
                    tarjan_stack[++stack_top] = {v, 0, 0};  // 7
        flag = true;
                    break;
        case 1 :                                            // 8
        flag = false;
                    low[u] = min(low[u], low[v]);
                } else if (ins[v]) {
                    low[u] = min(low[u], low[v]);
                }
#undef v
            }
        if (flag) break;                                     // 9
            if (dfn[u] == low[u]) {
                ++_col;
                do {
                    col[sta[_top]] = _col;
                    ins[sta[_top]] = 0;
                } while (sta[_top--] != u);
            }
        --stack_top;
        }
    }
}

以上代码如果只看三层缩进的代码,其实就几乎是递归版本的代码

一层缩进是模拟栈

二层缩进就是神奇的 switch 大法控制递归状态了(重点)

记住一个特性,如果 switch 没有 break, 那么匹配项(case) 之后的代码会一直执行下去,直到 break 或 switch 末尾为止

  1. 定义了个结构体用来存储函数运行的信息

    依次存了当前的结点 u

    循环到的边的信息 edge_info(本文存储的是边的序号,也可以存迭代器等等)

    函数的递归状态 state(后面详细讲)

  2. 因为 tarjan 函数不只执行一次,记得清空栈(虽然不清空一般没事)

    然后就是原本第一层的函数,将它入栈,开始模拟栈

  3. 这个 flag 标记很重要(重点)后面详细讲

  4. 取出栈里的信息,得到当前函数的信息

    注意我在这里采用了引用的形式,因为函数一般执行那么函数信息就会改变

    比如你遍历了一条边,该轮到下一条边了,栈里存储的信息也要对于改变,用引用就省去了记录

  5. 修改状态,下面讲

  6. 注意这里使用了宏替换,也是方便起见,为什么不能像原来一样呢?

    因为如果你直接跳转到了 case 1, 这个 v 就是未定义的

    不过方便的同时这个宏定义也非常危险(宏定义代码块内不得出现字符 v )

    当然最保险的办法还是老老实实写 e[u][i]

  7. 重点来了,递归部分,我们模拟栈,那么应该入栈(开辟下一层函数)

    然后我们应该暂时跳出当前函数,这里通过 flag 来实现

    这里真就是 switch 的神奇了.注意这里的 break, 跳出的是 for 循环(不是 case)

    也就是说我们来到了 9 的位置(通过简单的 gdb 调试易得)

    通过 break,我们就推出了 switch 也就相当于推出了当前函数

  8. 注意到 5, 这一层函数递归状态改为了 1, 如果没有进入下一层(7)

    就会继续执行下去没有影响

    但如果有下一层递归,我们知道下一层(7)执行完毕后要回到 8, state 的记录满足了这一条件,同时不要忘记把 flag 改回去

  9. 7 所述

如果不明白想一下所有情况

  1. if(!dfn[v]) 进入下一层递归,在 7 处入栈,跳出switch,下一层 while, 此时栈顶就是新加的函数

  2. if(dfn[v]) 跳到 elseif 正常执行

  3. 执行完了回退到上一层递归函数,会进入 case 1, 循环往复

如果还不明白呢,建议单步调试一下看一下过程(毕竟我只会口胡

总结

据此可以类比到任何形式递归改迭代了吧?(反正我不行(逃

总的来说并不是很好写吧(递归不香吗

如果不是对时间空间有限制的话(汇编手动扩栈不香吗

参考资料

github Yangff/dfs-benchmark