gpt4 book ai didi

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

转载 作者:太空狗 更新时间:2023-10-29 22:03:57 24 4
gpt4 key购买 nike

有没有一种方法可以防止我的 Ackerman 函数在流上创建堆栈是针对相对较小的数字,即 (4,2)。这是错误

{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;
}
}

最佳答案

避免StackOverflowException的最佳方法就是不使用栈。

让我们摆脱负面情况,因为当我们用 uint 调用时它没有意义.或者,如果我们在考虑其他可能性之前将否定测试作为方法中的第一件事,那么这里接下来的内容也将起作用:

首先,我们需要一艘更大的船:

    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));
}

现在成功至少在数学上是可能的。现在,n == 0 case 是一个足够简单的尾调用。让我们手动消除它。我们将使用 goto因为它是暂时的,所以我们不必担心迅猛龙或 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));
}

这已经需要更长的时间来炸毁堆栈,但是炸毁它,它会的。看这个表格,注意 m永远不会通过递归调用的返回来设置,而 n有时是。

对此进行扩展,我们可以将其转换为迭代形式,同时只需处理跟踪 m 的先前值。 ,我们将以递归形式返回的地方,我们分配给 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;
}

至此,我们已经回答了 OP 的问题。这将需要很长时间才能运行,但它会返回尝试过的值 (m = 4, n = 2)。它永远不会抛出 StackOverflowException , 虽然它最终会在超过 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();
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;
}

这对堆栈没有帮助,对堆也没有任何意义,但考虑到这个东西将处理大值的循环次数,我们可以削减的每一点都是值得的。

正在消除 goto在保持优化的同时留给读者作为练习:)

顺便说一句,我对这个测试太不耐烦了,所以我做了一个作弊形式,当 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;
}

使用此版本,我可以获得 true 的结果对于 Ackermann(4, 2) == BigInteger.Pow(2, 65536) - 3一秒多一点后(Mono,Release 构建,在 Core i7 上运行)。鉴于非作弊版本在为 m 的此类值返回正确结果方面是一致的。 ,我将此作为先前版本正确性的合理证据,但我将让它运行并查看。

编辑:当然,我真的不希望以前的版本在任何合理的时间范围内恢复,但我想我还是让它继续运行,看看它的内存使用情况如何。 6 小时后,它正好低于 40MiB。我很高兴,虽然显然不切实际,但如果在真机上有足够的时间,它确实会返回。

编辑:显然有人争论 Stack<T>达到其 2³¹ 项目的内部限制也算作一种“堆栈溢出”。如果必须,我们也可以处理:

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;
}

再次调用 Ackermann(4, 2)返回:

enter image description here

这是正确的结果。使用的堆栈结构永远不会抛出,因此唯一剩下的限制是堆(当然还有时间,如果输入足够大,您将不得不使用“宇宙生命周期”作为度量单位......)。

由于它的使用方式类似于图灵机的磁带,我们想起了一个论点,即任何可计算的函数都可以在足够大的图灵机上计算。

关于c# - 如何防止我的 Ackerman 函数溢出堆栈?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/12186672/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com