简体   繁体   English

如何防止我的Ackerman函数溢出堆栈?

[英]How can I prevent my Ackerman function from overflowing the stack?

Is there a way to keep my Ackerman function from creating a stack over flow is does it for relatively small numbers , ie (4,2). 有没有办法让我的Ackerman函数不会创建一个堆栈而不是流量是相对较小的数字,即(4,2)。 This is the error 这是错误

{Cannot evaluate expression because the current thread is in a stack overflow state.} {无法计算表达式,因为当前线程处于堆栈溢出状态。}

private void  Button1Click(object sender, EventArgs e)
        {
            var t = Ackermann(4,2);
            label1.Text += string.Format(": {0}", t);
            label1.Visible = true;
        }

        int Ackermann(uint m, uint n)
        {
            if (m == 0)
                return  (int) (n+1);
            if (m > 0 && n == 0)
                return Ackermann(m - 1, 1);
            if (m > 0 && n > 0)
                return Ackermann(m - 1, (uint)Ackermann(m, n - 1));
            else
            {
                return -1;
            }
        }

The best way to avoid StackOverflowException is to not use the stack. 避免StackOverflowException的最好方法是不使用堆栈。

Let's get rid of the negative case, as it's meaningless when we call with uint . 让我们摆脱负面情况,因为当我们用uint打电话时它没有意义。 Alternatively, what follows here will also work if we make the negative test the very first thing in the method, before the other possibilities are considered: 或者,如果我们在考虑其他可能性之前将负测试作为方法中的第一件事,那么此后的内容也将起作用:

First, we're going to need a bigger boat: 首先,我们需要一艘更大的船:

    public static BigInteger Ackermann(BigInteger m, BigInteger n)
    {
        if (m == 0)
            return  n+1;
        if (n == 0)
            return Ackermann(m - 1, 1);
        else
            return Ackermann(m - 1, Ackermann(m, n - 1));
    }

Now success is at least mathematically possible. 现在,成功至少在数学上是可行的。 Now, the n == 0 case is a simple enough tail-call. 现在, n == 0情况是一个足够简单的尾调用。 Let's eliminate that by hand. 让我们手工消除它。 We'll use goto because it's temporary so we don't have to worry about velociraptors or Dijkstra: 我们将使用goto因为它是临时的,所以我们不必担心velociraptors或Dijkstra:

    public static BigInteger Ackermann(BigInteger m, BigInteger n)
    {
    restart:
        if (m == 0)
            return  n+1;
        if (n == 0)
        {
            m--;
            n = 1;
            goto restart;
        }
        else
            return Ackermann(m - 1, Ackermann(m, n - 1));
    }

This will already take a bit longer to blow the stack, but blow it, it will. 这已经需要更长的时间来吹掉堆栈,但是它会吹掉它。 Looking at this form though, note that m is never set by the return of a recursive call, while n sometimes is. 但是,看一下这个表单,请注意m从不通过递归调用的返回来设置,而n有时是。

Extending this, we can turn this into an iterative form, while only having to deal with tracking previous values of m , and where we would return in the recursive form, we assign to n in our iterative form. 扩展这一点,我们可以将其转换为迭代形式,同时只需处理跟踪m先前值,以及我们将以递归形式返回的位置,我们以迭代形式分配n Once we run out of m s waiting to be dealt with, we return the current value of n : 一旦我们用完m等待处理,我们返回n的当前值:

    public static BigInteger Ackermann(BigInteger m, BigInteger n)
    {
        Stack<BigInteger> stack = new Stack<BigInteger>();
        stack.Push(m);
        while(stack.Count != 0)
        {
            m = stack.Pop();
            if(m == 0)
                n = n + 1;
            else if(n == 0)
            {
                stack.Push(m - 1);
                n = 1;
            }
            else
            {
                stack.Push(m - 1);
                stack.Push(m);
                --n;
            }
        }
        return n;
    }

At this point, we have answered the OP's question. 在这一点上,我们已经回答了OP的问题。 This will take a long time to run, but it will return with the values tried (m = 4, n = 2). 这将花费很长时间来运行,但它将返回所尝试的值(m = 4,n = 2)。 It will never throw a StackOverflowException , though it will end up running out of memory above certain values of m and n . 它永远不会抛出StackOverflowException ,尽管它最终会超出某些mn值的内存。

As a further optimisation, we can skip adding a value to the stack, only to pop it immediately after: 作为进一步的优化,我们可以跳过向堆栈添加一个值,只是在之后立即弹出:

    public static BigInteger Ackermann(BigInteger m, BigInteger n)
    {
        Stack<BigInteger> stack = new Stack<BigInteger>();
        stack.Push(m);
        while(stack.Count != 0)
        {
            m = stack.Pop();
        skipStack:
            if(m == 0)
                n = n + 1;
            else if(n == 0)
            {
                --m;
                n = 1;
                goto skipStack;
            }
            else
            {
                stack.Push(m - 1);
                --n;
                goto skipStack;
            }
        }
        return n;
    }

This doesn't help us with stack nor meaningfully with heap, but given the number of loops this thing will do with large values, every bit we can shave off is worth it. 这对堆栈没有帮助,也没有对堆有意义,但是考虑到循环的数量,这个东西会对大值进行处理,我们可以剃掉的每一点都是值得的。

Eliminating goto while keeping that optimisation is left as an exercise for the reader :) 在保持优化的同时消除goto仍然是读者的练习:)

Incidentally, I got too impatient in testing this, so I did a cheating form that uses known properties of the Ackerman function when m is less than 3: 顺便说一下,我对测试这个太不耐烦了,所以当m小于3时,我做了一个使用Ackerman函数的已知属性的作弊表:

    public static BigInteger Ackermann(BigInteger m, BigInteger n)
    {
        Stack<BigInteger> stack = new Stack<BigInteger>();
        stack.Push(m);
        while(stack.Count != 0)
        {
            m = stack.Pop();
        skipStack:
            if(m == 0)
                n = n + 1;
            else if(m == 1)
                n = n + 2;
            else if(m == 2)
                n = n * 2 + 3;
            else if(n == 0)
            {
                --m;
                n = 1;
                goto skipStack;
            }
            else
            {
                stack.Push(m - 1);
                --n;
                goto skipStack;
            }
        }
        return n;
    }

With this version, I can get a result of true for Ackermann(4, 2) == BigInteger.Pow(2, 65536) - 3 after a little over a second (Mono, Release build, running on a Core i7). 有了这个版本,我可以在Ackermann(4, 2) == BigInteger.Pow(2, 65536) - 3之后得到一个true的结果一秒钟之后(Mono,发布版本,在Core i7上运行)。 Given that the non-cheating version is consistent in returning the correct result for such values of m , I take this as reasonable evidence of the correctness of the previous version, but I'll leave it running and see. 鉴于非作弊版本在返回m这些值的正确结果时是一致的,我认为这是前一版本正确性的合理证据,但我会让它继续运行并看到。

Edit: Of course, I'm not really expecting the previous version to return in any sensible timeframe, but I thought I'd leave it running anyway and see how its memory use went. 编辑:当然,我并不是真的希望以前版本能够在任何合理的时间范围内返回,但我认为无论如何我都会让它继续运行,看看它的内存使用情况如何。 After 6 hours it's sitting nicely under 40MiB. 6个小时后,它的坐姿远低于40MiB。 I'm pretty happy that while clearly impractical, it would indeed return if given enough time on a real machine. 我很高兴虽然显然不切实际,但如果在真机上有足够的时间,它确实会回归。

Edit: Apparently it's being argued that Stack<T> hitting its internal limit of 2³¹ items counts as a sort of "stack overflow", too. 编辑:显然,有人认为Stack<T>达到2³¹项目的内部限制也算作一种“堆栈溢出”。 We can deal with that also if we must: 如果我们必须,我们也可以处理:

public class OverflowlessStack <T>
{
    internal sealed class SinglyLinkedNode
    {
        //Larger the better, but we want to be low enough
        //to demonstrate the case where we overflow a node
        //and hence create another.
        private const int ArraySize = 2048;
        T [] _array;
        int _size;
        public SinglyLinkedNode Next;
        public SinglyLinkedNode()
        {
            _array = new T[ArraySize];
        }
        public bool IsEmpty{ get{return _size == 0;} }
        public SinglyLinkedNode Push(T item)
        {
            if(_size == ArraySize - 1)
            {
                SinglyLinkedNode n = new SinglyLinkedNode();
                n.Next = this;
                n.Push(item);
                return n;
            }
            _array [_size++] = item;
            return this;
        }
        public T Pop()
        {
            return _array[--_size];
        }
    }
    private SinglyLinkedNode _head = new SinglyLinkedNode();

    public T Pop ()
    {
        T ret = _head.Pop();
        if(_head.IsEmpty && _head.Next != null)
            _head = _head.Next;
        return ret;
    }
    public void Push (T item)
    {
        _head = _head.Push(item);
    }
    public bool IsEmpty
    {
        get { return _head.Next == null && _head.IsEmpty; }
    }
}
public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
    var stack = new OverflowlessStack<BigInteger>();
    stack.Push(m);
    while(!stack.IsEmpty)
    {
        m = stack.Pop();
    skipStack:
        if(m == 0)
            n = n + 1;
        else if(m == 1)
            n = n + 2;
        else if(m == 2)
            n = n * 2 + 3;
        else if(n == 0)
        {
            --m;
            n = 1;
            goto skipStack;
        }
        else
        {
            stack.Push(m - 1);
            --n;
            goto skipStack;
        }
    }
    return n;
}

Again, calling Ackermann(4, 2) returns: 再次,致电Ackermann(4, 2)返回:

在此输入图像描述

Which is the correct result. 哪个是正确的结果。 The stack structure used will never throw, so the only limit remaining is the heap (and time of course, with large enough inputs you'll have to use "universe lifetime" as a unit of measurement...). 使用的堆栈结构永远不会抛出,因此剩余的唯一限制是堆(当然,当有足够大的输入时,您必须使用“Universe life”作为度量单位...)。

Since the way it's used is analogous to the tape of a Turing machine, we're reminded of the thesis that any calculable function can be calculated on a Turing machine of sufficient size. 由于它的使用方式类似于图灵机的磁带,我们想到的是,任何可计算的功能都可以在足够大小的图灵机上计算。

Use memoization. 使用memoization。 Something like: 就像是:

private static Dictionary<int, int> a = new Dictionary<int, int>();

private static int Pack(int m, int n) {
 return m * 1000 + n;
}

private static int Ackermann(int m, int n) {
  int x;
  if (!a.TryGetValue(Pack(m, n), out x)) {
    if (m == 0) {
      x = n + 1;
    } else if (m > 0 && n == 0) {
      x = Ackermann(m - 1, 1);
    } else if (m > 0 && n > 0) {
      x = Ackermann(m - 1, Ackermann(m, n - 1));
    } else {
      x = -1;
    }
    a[Pack(m, n)] = x;
  }
  return x;
}

However, this example only shows the concept, it will still not give the correct result for Ackermann(4, 2), as an int is way too small to hold the result. 但是,这个例子只显示了这个概念,它仍然没有给出Ackermann(4,2)的正确结果,因为int太小而无法保存结果。 You would need an integer with 65536 bits instead of 32 for that. 你需要一个65536位的整数而不是32位。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM