尝试使用延续传递样式以避免使用minimax算法进行堆栈溢出

时间:2018-03-29 21:32:35

标签: f# functional-programming stack-overflow minimax continuation-passing

我的目标摘要:弄清楚如何在使用算法时使用延续传递样式来避免堆栈溢出我认为不能使尾递归。或者,找到一种使函数尾递归的方法。

详细信息: 我是F#(和一般的函数式编程)的新手,我试图用alpha-beta修剪实现minimax算法。这是一种用于确定双人游戏的最佳移动的算法。可以在此处找到算法的伪代码:https://en.wikipedia.org/wiki/Alpha%E2%80%93beta_pruning

我发现这是一种有助于理解算法流程的资源:http://inst.eecs.berkeley.edu/~cs61b/fa14/ta-materials/apps/ab_tree_practice/

我的实施的一个不同之处在于,我正在进行的游戏的玩家并不总是交替。出于这个原因,我从函数中删除了一个参数。我的实现如下:

let rec minimax node depth alpha beta =
    if depth = 0 || nodeIsTerminal node then
        heuristicValue node
    else
        match node.PlayerToMakeNextChoice with
        | PlayerOneMakesNextChoice ->
            takeMax (getChildren node) depth alpha beta    
        | PlayerTwoMakesNextChoice ->
            takeMin (getChildren node) depth alpha beta
and takeMax children depth alpha beta =      
    match children with
    | [] -> alpha
    | firstChild :: remainingChildren ->
        let newAlpha = [alpha; minimax firstChild (depth - 1) alpha beta] |> List.max

        if beta < newAlpha then newAlpha
        else takeMax remainingChildren depth newAlpha beta
and takeMin children depth alpha beta =      
    match children with
    | [] -> beta
    | firstChild :: remainingChildren ->
        let newBeta = [beta; minimax firstChild (depth - 1) alpha beta] |> List.min

        if newBeta < alpha then newBeta
        else takeMax remainingChildren depth alpha newBeta

我遇到的问题是虽然takeMaxtakeMin是尾递归的,但这些方法在分配minimaxnewAlpha时会调用newBeta当我用大深度调用minimax时,它仍然可能产生堆栈溢出。我已经做了一些研究,发现使用延续传递样式是一种使用堆而不是堆栈的潜在方式,当函数不能被尾递归时(我相信经过几个小时的尝试后,这不可能)。虽然我可以理解一些非常基本的例子,但我很难理解如何将这个概念应用于这种情况;如果有人能帮助我完成它,我将非常感激。

编辑1:我对解决方案的最佳理解

let minimax node depth alpha beta =
    let rec recurse node depth alpha beta k =
        if depth = 0 || nodeIsTerminal node then
            k (heuristicValue node)
        else
            match node.PlayerToMakeNextChoice with
            | PlayerOneMakesNextChoice ->
                takeMax (getChildren node) depth alpha beta k
            | PlayerTwoMakesNextChoice ->
                takeMin (getChildren node) depth alpha beta k
    and takeMax children depth alpha beta k =      
        match children with
        | [] -> k alpha
        | firstChild :: remainingChildren ->
            let continuation = fun minimaxResult ->
                let newAlpha = [alpha; minimaxResult] |> List.max

                if beta < newAlpha then k newAlpha
                else takeMax remainingChildren depth newAlpha beta k

            recurse firstChild (depth - 1) alpha beta continuation
    and takeMin children depth alpha beta k =      
        match children with
        | [] -> k beta
        | firstChild :: remainingChildren ->
            let continuation = fun minimaxResult ->
                let newBeta = [beta; minimaxResult] |> List.min

                if newBeta < alpha then k newBeta
                else takeMax remainingChildren depth alpha newBeta k

            recurse firstChild (depth - 1) alpha beta continuation
    recurse node depth alpha beta id

1 个答案:

答案 0 :(得分:8)

正如您在&#34;基本示例&#34;中无疑看到的那样,一般的想法是采用一个额外的参数(&#34;延续&#34;,通常表示为k),是一个函数,每次返回一个值时,都会将该值传递给continuation。因此,举例来说,以这种方式修改minimax,我们得到:

let rec minimax node depth alpha beta k =
    if depth = 0 || nodeIsTerminal node then
        k (heuristicValue node)
    else
        match node.PlayerToMakeNextChoice with
        | PlayerOneMakesNextChoice ->
            k (takeMax (getChildren node) depth alpha beta)
        | PlayerTwoMakesNextChoice ->
            k (takeMin (getChildren node) depth alpha beta)

然后,呼叫网站需要&#34;内部翻转&#34;,可以这么说,所以不是这样的:

let a = minimax ...
let b = f a
let c = g b
c

我们会写这样的东西:

minimax ... (fun a ->
   let b = f a
   let c = g b
   c
)

请参阅? a曾经是minimax的返回值,但现在a是传递给minimax的延续的参数。运行时机制是,一旦minimax运行完毕,它就会调用此延续,其结果值将显示为参数a

因此,要将此应用于您的真实代码,我们会得到:

| firstChild :: remainingChildren ->
    minimax firstChild (depth - 1) alpha beta (fun minimaxResult ->
        let newAlpha = [alpha; minimaxResult] |> List.max

        if beta < newAlpha then newAlpha
        else takeMax remainingChildren depth newAlpha beta
    )

好的,这一切都很好,但这只是工作的一半:我们在CPS中重写了minimax,但takeMintakeMax仍然是递归的。不好。

所以让我们先做takeMax。同样的想法:添加一个额外的参数k,每次我们将&#34;返回&#34;一个值,将其传递给k代替:

and takeMax children depth alpha beta k =      
    match children with
    | [] -> k alpha
    | firstChild :: remainingChildren ->
        minimax firstChild (depth - 1) alpha beta (fun minimaxResult ->
            let newAlpha = [alpha; minimaxResult] |> List.max

            if beta < newAlpha then k newAlpha
            else takeMax remainingChildren depth newAlpha beta k
        )

现在,当然,我必须相应地修改呼叫站点:

let minimax ... k =
    ...
    match node.PlayerToMakeNextChoice with
    | PlayerOneMakesNextChoice ->
        takeMax (getChildren node) depth alpha beta k

等等,刚发生了什么?我只是说,每次我返回一个值时,我都应该将它传递给k,但在这里我没有这样做。相反,我将k本身传递给takeMax。咦?

嗯,&#34;而不是返回的规则传递给k&#34;这只是方法的第一部分。第二部分是 - &#34;在每次递归调用时,将k传递给链#34;。这样,原始顶级k将沿着整个递归调用链向下移动,并最终由任何函数决定停止递归调用。

请记住,尽管CPS有助于堆栈溢出,但它并不能解除内存限制。所有这些中间值都不再存在于堆栈中,但它们必须某处。在这种情况下,每次我们构造lambda fun minimaxResult -> ...时,都是堆分配。所以你的所有中间值都在堆上。

虽然有一个很好的对称性:如果算法真的是尾递归的,你就能够在调用链中传递原始的顶级延续,而不需要分配任何中间的lambdas,所以你不会#39 ; t需要任何堆内存。