“打结”是一种众所周知的技术,它可以让 GHC 运行时为您记住结果,如果您提前知道所有需要查找的值的话。这个想法是将你的递归函数变成一个自引用的数据结构,然后简单地查找你真正关心的值。我为此选择使用 Array,但 Map 也可以。在任何一种情况下,您使用的数组或映射都必须是惰性/非严格的,因为我们将向其中插入在整个数组被填满之前我们还没有准备好计算的值。
import Data.Array (array, bounds, inRange, (!))
paths :: Int -> Int -> Int -> Integer
paths m n k = go (1, 1, k, 0)
where go (i, j, k, dir)
| i == m && j == n = 1
| dir == 1 = get (i+1, j, k, 1) + get (i, j+1, k-1, 2) -- down was the direction took to reach here
| dir == 2 = get (i+1, j, k-1, 1) + get (i, j+1, k, 2) -- right was the direction took to reach here
| otherwise = get (i+1, j, k, 1) + get (i, j+1, k, 2) -- is in grid (1,1)
a = array ((1, 1, 0, 1), (m, n, k, 2))
[(c, go c) | c <- (,,,) <$> [1..m] <*> [1..n] <*> [0..k] <*> [1..2]]
get x | inRange (bounds a) x = a ! x
| otherwise = 0
我稍微简化了你的 API:
-
m 和 n 参数不会随着每次迭代而改变,因此它们不应成为递归调用的一部分
- 客户端不必告诉您
i、j 和dir 的开头是什么,因此它们已从函数签名中删除,并隐式分别从 1、1 和 0 开始
- 我还调换了
m 和n 的顺序,因为先使用n 参数很奇怪。这让我很头疼,因为我有一段时间没有注意到我还需要更改基本情况!
然后,正如我之前所说,我们的想法是用我们需要进行的所有递归调用来填充数组:这就是 array 调用。注意array 中的单元格是通过调用go 来初始化的,这(基本情况除外!)涉及调用get,这涉及在数组中查找元素。这样,a 是自引用或递归的。但是我们不必决定以什么顺序查找,或者以什么顺序插入它们:我们足够懒惰,GHC 会根据需要评估数组元素。
我也有点厚脸皮,只在数组中为dir=1 和dir=2 留出空间,而不是dir=0。我侥幸成功,因为dir=0 只在第一次调用时发生,我可以直接调用go,绕过get 中的边界检查。这个技巧确实意味着如果你传递一个小于 1 的 m 或 n 或小于零的 k,你会得到一个运行时错误。如果您需要处理这种情况,您可以为 paths 本身添加一个守卫。
当然,它确实有效:
> paths 3 3 2
4
您可以做的另一件事是为您的方向使用真实的数据类型,而不是 Int:
import Data.Array (Ix, array, bounds, inRange, (!))
import Prelude hiding (Right)
data Direction = Neutral | Down | Right deriving (Eq, Ord, Ix)
paths :: Int -> Int -> Int -> Integer
paths m n k = go (1, 1, k, Neutral)
where go (i, j, k, dir)
| i == m && j == n = 1
| otherwise = case dir of
Neutral -> get (i+1, j, k, Down) + get (i, j+1, k, Right)
Down -> get (i+1, j, k, Down) + get (i, j+1, k-1, Right)
Right -> get (i+1, j, k-1, Down) + get (i, j+1, k, Right)
a = array ((1, 1, 0, Down), (m, n, k, Right))
[(c, go c) | c <- (,,,) <$> [1..m] <*> [1..n] <*> [0..k] <*> [Down, Right]]
get x | inRange (bounds a) x = a ! x
| otherwise = 0
(I 和 J 可能是比 Down 和 Right 更好的名字,我不知道这更容易还是更难记住)。我认为这可能是一种改进,因为这些类型现在有了更多的意义,而且你没有这个奇怪的otherwise 子句来处理像dir=7 这样应该是非法的事情。但它仍然有点不稳定,因为它依赖于枚举值的顺序:如果我们将 Neutral 放在 Down 和 Right 之间,它会中断。 (我尝试完全删除Neutral 方向并为第一步添加更多特殊情况,但这会以自己的方式变得丑陋)