【问题标题】:converting a common recursion to tail recurtion because of stack overflow for large inputs由于大输入的堆栈溢出,将通用递归转换为尾递归
【发布时间】:2020-05-04 03:36:19
【问题描述】:

我为一个程序找到了这个算法,它返回具有特定周长的可能三角形的数量。我知道这个问题还有其他一些方法,但是使用它们我会得到大量的时间限制错误,可能是因为它们没有优化。这似乎更加优化,但我得到堆栈溢出递归大输入。任何人都可以通过将其转换为尾递归或对这个问题做其他事情来帮助我优化它吗?

这是我找到的算法:

#include<iostream>
using namespace std;
int foo(int);
int main(){
    int n;
    cin>>n;
    cout<<foo(n);
    return 0;
}
int foo(int n){
    int p=n/2-2;
    int t;
    if(p%6<5&&p%6>0)
        t=(p+6-p%6)/6;
    else if(p%6==5)
        t=(p+6-p%6)/6+1;
    else
        t=p/6;
    if(n==3||n==5||n==6)
        return 1;
    else if(n==4)
        return 0;
    else{
        if(n%2==0)
            return foo(n-3);
        else
            return foo(n+1)+t;
    }
}

【问题讨论】:

  • 恭喜,您的代码可以编译。现在,请提供一些具有预期结果的测试输入,以实现minimal reproducible example。我知道它目前看起来相当小,但请查看“stackoverflow.com/help/minimal-reproducible-example”......我们需要知道哪些输入会重现您遇到的错误。
  • 你如何说服自己代码是尾递归的还是不是尾递归的?
  • 你指定了哪些编译选项......我想我从未见过 -O0 输出尾递归可执行文件。
  • 考虑查看stackoverflow.com/a/58324382/2785528,了解如何理解递归函数的执行顺序。
  • 我投票决定关闭主题,因为您的问题不属于帮助中心定义的范围。如果没有输入和预期输出,问题就无法重现。

标签: c++ recursion tail-recursion


【解决方案1】:

问题就在这里:

return foo(n+1) + t;
//              ^^^

添加使对foo 的递归调用不是函数中发生的最后一件事。因此,您需要将该添加 移到 函数中:

return foo(n + 1, t);

为了使其他递归调用兼容,您只需提供加法的中性元素:

return foo(n-3, 0);

新函数的签名:

int foo(int n, int offset = 0); // default parameter allows for calling just as before.

剩下的最后一个问题:在哪里做加法???

嗯,有两个地方很明显:

if(n==3||n==5||n==6)
    return 1 + offset;
else if(n==4)
    return /*0 +*/ offset;

offset 是之前所有递归调用的累积偏移量!所以你必须将它添加到你添加到下一个函数的递归调用的任何内容中,所以:

// ...
else if(n % 2 == 0)
    return foo(n-3, /*0 +*/ offset);
else
    return foo(n+1, t + offset);

旁注:您的算法似乎不适用于负数。使用这些调用(未修改)foo 最终会重复出现,直到堆栈溢出(如果堆栈足够大,可能会由于有符号整数溢出而导致未定义的行为)。如果您不打算在负数上使用它,绝对应该使用unsigned int 作为数据类型!否则,它需要适当的修复。

值 0、1 和 2 也需要特殊处理,它们会导致负值 n(对于未修改的 foo,请参见上文)。但你几乎可以轻松地做到这一点:

if(n == 4)
{ ... }
else if (n < 6)
{ ... }

如果 n 为 0、1 或 2,则返回 1(即使使用尾调用优化版本,因为偏移量仍为 0),甚至可以进行一些比较。如果您需要为特殊值返回 0:也很简单:

else if (n < 6)
    return (n >= 3) + offset;

您甚至可以将所有内容合并为一个条件:

if(n < 6)
    return (n == 3 || n > 4) + offset; // returning 0 for 0, 1, 2
    return (n <= 3 || n > 4) + offset; // returning 1 for 0, 1, 2

最大的优势:在所有这些递归中,您只需要一次检查,而所有其他检查仅在满足停止条件时完成(即只需一次)。

而且您可以将 pt 的计算移到停止条件后面,无论如何您都不要在那里使用它们...

【讨论】:

  • 对于像我这样想知道return t+foo(n+1); 是否有用的人:将其阅读为return operator+(t, foo(n+1));,很明显,为什么它不起作用。
  • 你知道我认为问题不存在,因为例如输入 200000 IDE 突出显示行 return foo(n-3);
  • @MaryamSadatRazaviTaheri 哪个问题?你的意思是输入0、1、2?最初使用较大的值调用时,您可能不会遇到这些值。但是最初有人可能会尝试使用这些值测试函数(在非递归调用中)。因此,也许您可​​能仅在未给出这些值的情况下才在调用递归变体的辅助函数中检查一次。至少非常非常您必须记录该函数在这些输入值上调用未定义的行为。
  • @MaryamSadatRazaviTaheri 如果你没有负输入,你可以简化你对p的检查:if(p % 6 != 5) { } else { }:如果p是正数,那么模数也会是正数,所以你不要'不必检查
  • @MaryamSadatRazaviTaheri 一些微调使附​​加函数过时,甚至减少了递归调用中的比较......
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-09-27
  • 2019-12-21
  • 2015-04-04
  • 2017-01-20
  • 2018-12-02
相关资源
最近更新 更多