【问题标题】:No idea how to solve SICP exercise 1.11不知道如何解决 SICP 练习 1.11
【发布时间】:2011-01-22 21:16:12
【问题描述】:

Exercise 1.11:

函数ff(n) = n 如果n < 3f(n) = f(n - 1) + 2f(n - 2) + 3f(n - 3) 如果n > 3 定义。编写一个通过递归过程计算f 的过程。编写一个通过迭代过程计算f 的过程。

递归实现它很简单。但我不知道如何迭代地做到这一点。我尝试与给出的斐波那契示例进行比较,但我不知道如何将其用作类比。所以我放弃了(对我感到羞耻)并在谷歌上搜索an explanation,我发现了这个:

(define (f n)
   (if (< n 3)
       n
       (f-iter 2 1 0 n)))

(define (f-iter a b c count)
   (if (< count 3)
       a
       (f-iter (+ a (* 2 b) (* 3 c))
               a
               b
               (- count 1))))

阅读后,我理解了代码及其工作原理。但我不明白的是从函数的递归定义到 this 所需的过程。我不明白代码是如何在某人的脑海中形成的。

您能解释一下得出解决方案所需的思考过程吗?

【问题讨论】:

  • @Javier ,我在程序中的这一行遇到问题(如果 (时才会调用f-iter 3.我得到了a1=f(2),a2=f(3),所以'a'只存在于n=3之后,但无法连接点。
  • @Javier 我现在意识到循环将为 a 分配新值。

标签: recursion scheme iteration sicp


【解决方案1】:

您需要在一些累加器中捕获状态并在每次迭代时更新状态。

如果您有使用命令式语言的经验,想象一下编写一个 while 循环并在循环的每次迭代期间跟踪变量中的信息。你需要什么变量?您将如何更新它们?这正是您在 Scheme 中进行迭代(尾递归)调用集所必须做的。

换句话说,开始将其视为一个while循环而不是递归定义可能会有所帮助。最终,您将足够流利地使用递归 -> 迭代转换,无需额外帮助即可开始使用。


对于这个特定的示例,您必须仔细查看三个函数调用,因为目前尚不清楚如何表示它们。但是,可能的思考过程如下:(用 Python 伪代码强调命令性)

每个递归步骤都跟踪三件事:

f(n) = f(n - 1) + 2f(n - 2) + 3f(n - 3) 

所以我需要三个状态来跟踪f 的当前值、最后一个值和倒数第二个值。 (即f(n-1), f(n-2) and f(n-3)。)称他们为a, b, c。我必须在每个循环中更新这些部分:

for _ in 2..n:
    a = NEWVALUE
    b = a
    c = b
return a

那么什么是 NEWVALUE?好吧,现在我们有了f(n-1), f(n-2) and f(n-3) 的表示形式,这只是递归方程:

for _ in 2..n:
    a = a + 2 * b + 3 * c
    b = a
    c = b
return a

现在剩下的就是找出a, b and c 的初始值。但这很容易,因为我们知道f(n) = n if n &lt; 3

if n < 3: return n
a = 2 # f(n-1) where n = 3
b = 1 # f(n-2)
c = 0 # f(n-3)
# now start off counting at 3
for _ in 3..n:
    a = a + 2 * b + 3 * c
    b = a
    c = b
return a

这和Scheme迭代版本还是有点区别的,但是希望你现在能看到思路。

【讨论】:

  • +1 - 我希望我在第一次发布这个问题时就看到了这个问题,但我认为我自己无法更好地解释它。
  • b = old_a; c = old_b
【解决方案2】:

由于您链接到的帖子描述了很多有关解决方案的信息,因此我将尝试仅提供补充信息。

在给定(非尾)递归定义的情况下,您正在尝试在 Scheme 中定义尾递归函数。

递归的基本情况(f(n) = n 如果 n

(define (f n)
   (f-iter 2 1 0 n))

一般形式是:

(define (f-iter ... n)
   (if (base-case? n)
       base-result
       (f-iter ...)))

请注意,我还没有为 f-iter 填写参数,因为您首先需要了解需要将什么状态从一个迭代传递到另一个迭代。

现在,让我们看看递归形式的 f(n) 的依赖关系。它引用了 f(n - 1)、f(n - 2) 和 f(n - 3),因此我们需要保留这些值。当然,我们需要 n 本身的值,所以我们可以停止对其进行迭代。

这就是你提出尾递归调用的方式:我们计算 f(n) 以用作 f(n - 1),将 f(n - 1) 旋转到 f(n - 2) 和 f(n - 2) 到 f(n - 3),并递减计数。

如果这仍然没有帮助,请尝试问一个更具体的问题——当你写“我不明白”时,已经给出了相对详尽的解释,这真的很难回答。

【讨论】:

    【解决方案3】:

    我认为您是在问如何在“设计模式”之外自然地发现算法。

    查看 f(n) 在每个 n 值处的展开对我很有帮助:

    f(0) = 0  |
    f(1) = 1  | all known values
    f(2) = 2  |
    
    f(3) = f(2) + 2f(1) + 3f(0)
    f(4) = f(3) + 2f(2) + 3f(1)
    f(5) = f(4) + 2f(3) + 3f(2)
    f(6) = f(5) + 2f(4) + 3f(3)
    

    仔细观察 f(3),我们发现我们可以立即从已知值计算它。 计算 f(4) 需要什么?

    我们至少需要计算 f(3) + [其余的]。但是当我们计算 f(3) 时,我们也计算 f(2) 和 f(1),我们恰好需要计算 f(4) 的 [其余部分]。

    f(3) = f(2) + 2f(1) + 3f(0)
                ↘       ↘
    f(4) = f(3) + 2f(2) + 3f(1)
    

    因此,对于任何数字 n,我都可以从计算 f(3) 开始,然后重用我用来计算 f(3) 的值来计算 f(4)...并且模式继续...

    f(3) = f(2) + 2f(1) + 3f(0)
                ↘       ↘
    f(4) = f(3) + 2f(2) + 3f(1)
                ↘       ↘
    f(5) = f(4) + 2f(3) + 3f(2)
    

    因为我们将重用它们,所以我们给它们命名为 a、b、c。下标我们正在进行的步骤,并通过 f(5) 的计算:

    第 1 步:f(3) = f(2) + 2f(1) + 3f(0) 或 f(3) = a1 + 2b1 +3c1

    在哪里

    a1 = f(2) = 2,

    b1 = f(1) = 1,

    c1 = 0

    因为 n

    因此:

    f(3) = a1 + 2b1 + 3c1 = 4

    第二步:f(4) = f(3) + 2a1 + 3b1

    所以:

    a2 = f(3) = 4(在上面的步骤1中计算),

    b2 = a1 = f(2) = 2,

    c2 = b1 = f(1) = 1

    因此:

    f(4) = 4 + 2*2 + 3*1 = 11

    第三步:f(5) = f(4) + 2a2 + 3b2

    所以:

    a3 = f(4) = 11(在上面的步骤2中计算),

    b3 = a2 = f(3) = 4,

    c3 = b2 = f(2) = 2

    因此:

    f(5) = 11 + 2*4 + 3*2 = 25

    在上面的计算中,我们捕获了前面计算中的状态并将其传递给下一步, 特别是:

    astep = 步骤的结果 - 1

    bstep = astep - 1

    cstep = bstep -1

    当我看到这一点后,想出迭代版本就很简单了。

    【讨论】:

      【解决方案4】:

      我将以与此处的其他答案略有不同的方式来解决这个问题,重点关注编码风格如何使这样的算法背后的思维过程更容易理解。

      您的问题中引用的Bill's approach 的问题在于,目前尚不清楚状态变量abc 所传达的含义是什么。他们的名字没有传达任何信息,比尔的帖子也没有描述他们遵守的任何不变量或其他规则。如果状态变量遵循一些描述它们相互关系的记录规则,我发现更容易制定和理解迭代算法。

      考虑到这一点,请考虑完全相同算法的替代公式,它与比尔的不同之处仅在于为 abc 提供更有意义的变量名称以及递增计数器变量而不是递减一:

      (define (f n)
          (if (< n 3)
              n
              (f-iter n 2 0 1 2)))
      
      (define (f-iter n 
                      i 
                      f-of-i-minus-2
                      f-of-i-minus-1
                      f-of-i)
          (if (= i n)
              f-of-i
              (f-iter n
                      (+ i 1)
                      f-of-i-minus-1
                      f-of-i
                      (+ f-of-i
                         (* 2 f-of-i-minus-1)
                         (* 3 f-of-i-minus-2)))))
      

      突然间,算法的正确性——以及其创建背后的思维过程——变得很容易看到和描述。计算f(n)

      • 我们有一个计数器变量 i,它从 2 开始上升到 n,每次调用 f-iter 时递增 1。
      • 在此过程中的每一步,我们都会跟踪f(i)f(i-1)f(i-2),这足以让我们计算f(i+1)
      • 一旦i=n,我们就完成了。

      【讨论】:

      • 对我来说,恰恰相反。 f-of-i-minus-1f-of-i-minus-2 几乎听不见; (n, a, b, c) -&gt; (n+1, a+2*b+3*c, a, b) 是直观且不言自明的。 a, b, c各有各的优点;只能在一起。
      • (...所以,不值得任何特殊命名)
      【解决方案5】:

      函数ff(n) = n, if n&lt;3f(n) = f(n - 1) + 2f(n - 2) + 3f(n - 3), if n &gt; 3 的规则定义。编写一个通过递归过程计算f 的过程。

      已经写好了:

      f(n) = n,                                  (* if *)  n < 3
           = f(n - 1) + 2f(n - 2) + 3f(n - 3),   (* if *)  n > 3
      

      信不信由你,曾经有过such a language。用另一种语言写下来只是语法问题。顺便说一句,您(错误)引用它的定义有一个错误,现在非常明显和清楚。

      编写一个通过迭代过程计算f 的过程。

      迭代的意思是going forward这里有你的解释!)而不是递归的开始向后到最低级别,然后继续计算备份途中的结果:

      f(0)   =  0
      f(1)   =  1
      f(2)   =  2
      f(n)   =  f(n - 1) + 2f(n - 2) + 3f(n - 3) 
             =  a        + 2b        + 3c
      f(n+1) =  f(n    ) + 2f(n - 1) + 3f(n - 2)
             =  a'       + 2b'       + 3c'         where
                                                   a' = f(n) = a+2b+3c, 
                                                   b' = f(n-1) = a, 
                                                   c' = f(n-2) = b
      ......
      

      因此,这将问题的状态转换描述为

       (n, a, b, c)  ->  (n+1, a+2*b+3*c, a, b)
      

      我们可以把它编码为

      g (n, a, b, c) = g (n+1, a+2*b+3*c, a, b)
      

      但当然它永远不会停止。所以我们必须改为

      f n = g (2, 2, 1, 0)
        where
        g (k, a, b, c) = g (k+1, a+2*b+3*c, a, b),    (* if *) k < n
        g (k, a, b, c) = a,                           otherwise 
      

      这已经和你问的代码完全一样了,就语法而言。

      按照我们的“前进”范式,在这里计数到 n 更自然,但是就像您引用的代码一样,倒数到 0 当然完全是等价的。

      由于练习不感兴趣的技术细节,而忽略了极端情况和可能的错误。

      【讨论】:

        【解决方案6】:

        帮助我的是使用铅笔手动运行该过程并使用作者为斐波那契示例提供的提示

        a <- a + b
        b <- a
        

        将此转化为新问题是您如何在流程中推动状态

        a <- a + (b * 2) + (c * 3)
        b <- a
        c <- b
        

        所以你需要一个带有接口的函数来接受 3 个变量:a, b, c。它需要使用上面的过程调用自己。

        (define (f-iter a b c)
          (f-iter (+ a (* b 2) (* c 3)) a b))
        

        如果您从(f-iter 1 0 0) 开始为每次迭代运行并打印每个变量,您将得到类似的结果(当然它会永远运行):

        a   b   c
        =========
        1   0   0
        1   1   0
        3   1   1
        8   3   1
        17  8   3
        42  17  8
        100 42  17
        235 100 42
        ...
        

        你能看到答案吗?您可以通过对每次迭代的 b 和 c 列求和来获得它。我必须承认我是通过一些跟踪和错误发现的。剩下的就是有一个计数器知道何时停止,这就是全部:

        (define (f n)
          (f-iter 1 0 0 n))
        
        (define (f-iter a b c count)
          (if (= count 0)
              (+ b c)
              (f-iter (+ a (* b 2) (* c 3)) a b (- count 1))))
        

        【讨论】:

        • 太棒了,谢谢!
        猜你喜欢
        • 1970-01-01
        • 2012-07-05
        • 2011-11-05
        • 2017-02-03
        • 2010-12-26
        • 2012-12-15
        • 2021-05-23
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多