【问题标题】:TCO optimized Tower of Hanoi in ClojureClojure 中 TCO 优化的河内塔
【发布时间】:2026-02-10 05:55:02
【问题描述】:

我正在阅读 Introdution to Haskell 课程,他们正在介绍众所周知的河内塔问题作为第一堂课的作业。我动心了,写了一个解决方案:

type Peg = String

type Move = (Peg, Peg)

hanoi :: Int -> Peg -> Peg -> Peg -> [Move]
hanoi n b a e
  | n == 1 = [(b, e)]
  | n > 1 = hanoi (n - 1) b e a ++ hanoi 1 b a e ++ hanoi (n - 1) a b e
  | otherwise = []

我玩了一点,发现它显然使用了尾调用优化,因为它在恒定内存中工作。

Clojure 是我大部分时间都在使用的语言,因此我面临着编写 Clojure 解决方案的挑战。幼稚的被丢弃,因为我想写它来使用 TCO:

(defn hanoi-non-optimized
  [n b a e]
  (cond
    (= n 1) [[b e]]
    (> n 1) (concat (hanoi-non-optimized (dec n) b e a)
                    (hanoi-non-optimized 1 b a e)
                    (hanoi-non-optimized (dec n) a b e))
    :else   []))

嗯,Clojure 是 JVM 托管的,因此默认情况下没有 TCO,应该使用 recur 来获取它(我知道这个故事......)。另一方面,recur 施加了一些语法约束,因为它必须是最后一个表达式 - 必须是尾部。我感觉有点糟糕,因为我仍然无法编写一个像 Haskell 中那样简短/富有表现力的解决方案并同时使用 TCO。

是否有一个我目前看不到的简单解决方案?

我非常尊重这两种语言,并且已经知道这是我的方法而不是 Clojure 本身的问题。

【问题讨论】:

    标签: haskell recursion clojure tail-recursion towers-of-hanoi


    【解决方案1】:

    不,Haskell 代码不是尾递归的。它是受保护的-递归的,递归由惰性数据构造函数:++ 调用最终转换为)保护,其中由于惰性,只有递归调用的一部分树 (a ++ b ++ c) 被轮流探索,因此堆栈的深度永远不会超过 n,即磁盘的数量。非常小,比如 7 或 8。

    因此,Haskell 代码探索 a,将 c 部分放在一边。另一方面,您的 Clojure 代码计算连接它们的两个部分(ac,因为 b 不计算在内)连接它们之前,所以是双递归的,即计算量很大。

    您要寻找的不是 TCO,而是 TRMCO -- tail recursion modulo cons 优化 -- 即从带有模拟堆栈的循环内部以自上而下的方式构建列表。 Clojure 尤其适合这种情况,它的尾部附加 conj(对吗?)而不是 Lisp 和 Haskell 的头部附加 cons

    或者只是打印动作而不是构建所有动作的列表。

    编辑: 实际上,TRMCO 意味着如果我们自己维护“继续堆栈”,我们就可以重用调用帧,因此堆栈深度正好是 1。在这种情况下,Haskell 很可能会构建一个嵌套 ++ thunk 节点的左加深树,正如 here 所解释的那样,但在 Clojure 中,我们可以将其重新排列到右嵌套 list我们自己,当我们维护自己的 to-do-next 调用描述堆栈时(对于a ++ b ++ c 表达式的bc 部分)。

    【讨论】:

    • 感谢您的参考。但是,当我使用ghc-8.0.1 编译 Haskell 函数并使用更多磁盘(例如 9999)运行它时,它永远不会超过内存。实际上,处理器负载表现得很奇怪,但内存是恒定的,不会发生堆栈溢出。据我了解,您说它应该导致更大的ns 的堆栈溢出?
    • 我的理解是,对于n 磁盘,递归深度也将是n。我希望10000不会太多。您是否使用+RTS -s 开关运行独立可执行文件,用于“统计”?还有更完整的分析选项,您必须能够在 SO 上找到更多相关信息。
    • 我们可以看看你的 Clojure TRMCO 版本吗?
    • @Thumbnail 这必须是一个带有模拟堆栈的显式循环,手动实现支持 TRMCO 的编译器会产生什么。稍后我会尝试做点什么。
    • Don Knuth 可能已经在 1974 年的 Structured Programming with Goto Statements 中预料到了这一点。请看从 p20/journal-page 280 开始的示例 6。