【问题标题】:Trouble understanding / visualising SICP streams Hamming numbers program无法理解/可视化 SICP 流 Hamming 数字程序
【发布时间】:2019-09-02 07:07:13
【问题描述】:

我基本上被困在 SICP 的练习 3.56 上。问题是这样的:

练习 3.56。一个著名的问题,首先由 R. Hamming 提出,是按不重复的升序枚举所有除 2、3 或 5 之外没有素因数的正整数。一个明显的方法是简单地测试每个整数反过来看它是否有除 2、3 和 5 之外的任何因子。但这非常低效,因为随着整数变大,符合要求的整数越来越少。作为替代方案,让我们调用所需的数字流 S 并注意以下有关它的事实。

  • S 以 1 开头。
    • (scale-stream S 2) 的元素也是 S 的元素。
    • 对于 (scale-stream S 3) 和 (scale-stream 5 S) 也是如此。
    • 这些都是S的元素。

现在我们要做的就是组合来自这些来源的元素。为此,我们定义了一个过程合并,将两个有序流合并为一个有序结果流,消除重复:

(define (merge s1 s2)
   (cond ((stream-null? s1) s2)
         ((stream-null? s2) s1)
         (else
          (let ((s1car (stream-car s1))
                (s2car (stream-car s2)))
            (cond ((< s1car s2car)
                   (cons-stream s1car (merge (stream-cdr s1) s2)))
                  ((> s1car s2car)
                   (cons-stream s2car (merge s1 (stream-cdr s2))))
                  (else
                   (cons-stream s1car
                                (merge (stream-cdr s1)
                                       (stream-cdr s2)))))))))

然后可以用merge构造出需要的流,如下:

(define S (cons-stream 1 (merge &lt;??&gt; &lt;??&gt;)))

在上面标记的地方填上缺失的表达式。

在这个特殊问题之前,我已经能够使用信号处理框图来可视化和理解这些隐式流定义,并将原始流反馈到过程。

但我基本上遇到了这个特殊问题,我已经查找了解决方案,但我发现无法想象解决方案在我的头脑/论文中的工作方式。

有没有什么诀窍可以帮助你理解这些问题并想出解决方案?

这是有效的解决方案:

(define S 
  (cons-stream 1 (merge (scale-stream S 2)
                        (merge (scale-stream S 3)
                               (scale-stream S 5)))))

提前致谢。

【问题讨论】:

  • 既然您已经看到了答案,您是否尝试过在评估它们之前了解Scheme 如何扩展这些表达式?我认为这可以帮助您理解这里发生了什么(使用书中给出的 cons-stream 的等效定义,在您手动扩展它时使用延迟。)我建议至少在您完成流的扩展之前达到 6(流中的最小数字,它是两个不同因子的倍数)。
  • 尝试使用显式对象对其进行编码,表示为具有可变状态的闭包,它显式地从供应商那里提取输入以产生输出(作为一种可能的生成器模型)。你会在这里发现很多隐藏的东西、可能性和选择(参见 Python 的 tee 函数及其复杂性)。然后,切换回流,您将能够了解流是如何自动(和/或更好地)完成它的,甚至可以看到可能的流实现中的不同选择。

标签: stream scheme lazy-evaluation sicp hamming-numbers


【解决方案1】:

作为正确命名的问题,merge 不应该删除重复项,因为它的名字暗示它是应该保留它们的 mergesort 的一部分。 Union 是此类操作的更好名称,它通过增加唯一数字列表来查看(此处)表示的集合,它应该通过删除只能来自其 both 的重复项来保留该约束论据。

回到问题本身,我们象征性地写成

S<sub>235</sub> = {1} ∪ 2*S<sub>235</sub> ∪ 3*S<sub>235</sub> ∪ 5*S<sub>235</sub>

Premature implementation is the mother of all evil! (等等,什么?)我们甚至还不会尝试确定这些s 究竟是如何工作的,甚至不是命令。或者甚至有多少个术语:

S<sub>23</sub> = {1} ∪ 2*S<sub>23</sub> ∪ 3*S<sub>23</sub>

甚至

S<sub>2</sub> = {1} ∪ 2*S<sub>2</sub>

现在这看起来很简单。我们甚至可以在这里伪实现AB 的联合简单,首先,获取A 的所有元素,然后-- 的B。它会在这里正常工作,因为 的左侧输入中只有一个元素:

 {1} ----∪-->--->--S₂--.--->S₂
        /               \        
        \______*2_______/        
          ---<----<---         

这是如何工作的? 1 进入 combiner,首先退出它,无条件(注意这个发现的要求很重要,因为如果 有为了立即检查它的两个论点,我们会给自己一个无限循环,一个在 Haskell argot 中的 黑洞),被. splitter 一分为二,然后1 的第一个副本继续前进到输出点,而1 的第二个副本通过*2 乘数返回,生成的2 这次在右侧返回unopposed 被左边的任何东西(此时已经是空的),并以同样的方式继续,所以2 到达输出点,然后是4,然后是8,等等。等等。

换一种说法,S₂包含{1}的所有元素;加上{1} 中所有通过*2 乘数一次的元素;两次;和三倍;依此类推——2 的所有权力按递增顺序排列:

S2 = {1} ∪ 2*{1} ∪ 2*2*{1}                ;; == {1, 2, 4, 8, 16, 32, ...}
                 ∪ 2*2*2*{1}
                 ∪ 2*2*2*2*{1}
                 ∪ ..........

图中的两个S₂ 是相同的,因为我们在分流点从它虹吸的任何东西都不会影响它。

这不是很有趣吗?

那么我们如何将3 的倍数加到它上面呢?一种方法是

S<sub>23</sub> = S<sub>2</sub> ∪ 3*S<sub>23</sub>

 {1} ----∪-->--->--S₂--.---S₂----∪-->--->--S₂₃--.--->S₂₃
        /               \       /                \        
        \______*2_______/       \______*3________/        
          ---<----<---            ---<----<---         

这里来自S₂1 进入第二个 组合器并继续到输出点S₂₃ 以及通过*3 乘数返回,变成3。现在第二个2,4,8,...3,... 作为它的输入; 2 经历并变成6。接下来,4,8,16,...3,6,...3 通过。接下来,4;等等等等等等。

因此S₂ 的所有元素都是S₂₃ 的一部分,但S₂ 的所有元素也是如此,它们通过*3 乘数一次、两次等等, -- 2 和 3 按升序相乘:

S23 = S2 ∪ 3*S2 ∪ 3*3*S2                   ;; = S2 ∪ 3*( S2 ∪ 3*S2 
                ∪ 3*3*3*S2                 ;;               ∪ 3*3*S2 
                ∪ 3*3*3*3*S2               ;;               ∪ 3*3*3*S2 
                ∪ ..........               ;;               ∪ ........ )   !!

为什么是递增顺序?如何?为什么,那是的责任!您好,另一个发现的需求。无论从哪一侧进入它,它都必须在较大的元素之前产生较小的元素。

如果两者相等,该怎么办?在这个方案中,我们甚至需要关心这个问题吗?这会发生吗,在这里?

不能。所以我们可以将 here 实现为merge,而不是union(但记住第一个发现的需求!--它仍然有效吗?需要吗?新增病例)。 Merge 应该比 union 更有效,因为它不关心 equals 的情况。

对于 5 的倍数呢?我们继续,因为

S<sub>235</sub> = S<sub>23</sub> ∪ 5*S<sub>235</sub>

 {1} ----∪-->--->--S₂--.---S₂----∪-->--->--S₂₃--.---S₂₃----∪-->--->--S₂₃₅--.--->S₂₃₅
        /               \       /                \         /                 \ 
        \______*2_______/       \______*3________/         \_______*5________/ 
          ---<----<---            ---<----<---                ---<----<---     
  • 这是否描述了书中的代码? _______
  • 这是否描述了一个比书中的代码快两倍的代码? _______
  • 为什么它比书中的代码快两倍? _______
  • 这是否回答了您的问题? _______
  • 这是否有助于回答您的问题? _______

(填空)。

另见:


本书代码的信号处理框图为:

                                  1 --->---\
                                             cons-stream ->-- S ---.---> S
    /----->---->--- *2 --->---\            /                       |
   /                            union ->--/                        /
  .-->-- *3 -->--\            /                                   /
  |                union ->--/                                   /
  .-->-- *5 -->--/                                              /
  \                                                            /
   \__________<__________<__________<_________<_______________/

去除重复的“联合”在书中称为merge

【讨论】:

  • 在我看来,这本书通常同时处理相当抽象的想法和实现细节。这是从“上方”对问题的一个非常清晰的看法,很高兴看到从问题分析中演变出的解决方案。一般来说,我仍然从“下面”看问题,试图理解这些流背后的实现和魔力,试图“学习技巧”如何简单的过程,如 delay force 可以允许这样优雅的解决方案。当我用“企业”Java 度过一天时,这具有挑战性。 :-)
  • @codybartfast 是的,我明白你的意思。出于某种原因(也许是出于教育原因),我们(我也是指我,也包括你)通常是势在必行的。正如您所描述的,我也首先在命令式理解上花费了很大的精力。直到我写下这个答案,我才能清楚地“简单地”向自己解释这一点。 :) 如果您首先关注 S2 示例,则可以以命令式(即一对一的方式)和声明式(即“值集”)来阅读它——因为集合 {1} 具有 one 元素! :) 诀窍在于合并/联合保留相关不变量:1/
  • @codybartfast 两个升序in,一个升序out。因此,两种可能的实现方式,联合和合并,取决于两个序列中是否可以存在某个数字(假设已经没有重复)。 (当然,merge 可以看作是“两个非降序输入,一个非降序输出”,实现完全相同,但此处不相关)。如果您想在此答案中添加一些内容,或者有特定问题,请随时提问。 /2
  • @codybartfast 实际上这些代码 sn-ps/ 图是对工作算法的精确伪代码描述。你只需要实现我描述的U,它可以与书中的merge(即“联合”)几乎完全相同,只有一个关键区别:它们需要通过第一个元素无条件地,即在将右参数拉到头部和流的其余部分之前。 (在这里使用类似“联合”的比较效率较低。)
  • @codybartfast 好吧,它现在就在这里,希望会永远存在,因此您可以稍后随时返回。 :) 你可以在谷歌上搜索关于 SICP 及其练习的网站,甚至还有一个关于它们的 wiki,IIRC。快乐的小径!并且每当您遇到障碍时,发布一个 SO 问题!
【解决方案2】:

这是我将其可视化的最佳尝试。但我确实很挣扎,感觉就像一条三头蛇在吃自己的尾巴。

If we say the values of the stream S are s0, s1, s2, ..., then 
initially we only know the first value, s0.

               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    ?    ?    ?    ?    ?    ?    ?    ?    ?    ?  

But we do know the three scale-streams will be producing multiples of
these values, on demand:

               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    ?    ?    ?    ?    ?    ?    ?    ?    ?    ?  

    scale-2:   2*1  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?
    scale-3:   3*1  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:   5*1  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________


Merge will initially select the lowest of the numbers at the heads of
these three streams, forcing their calculation in the process:

               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    ?    ?    ?    ?    ?    ?    ?    ?    ?    ?  

    scale-2:  [2]  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?
    scale-3:   3   3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:   5   5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________


 So s1 will now have the value 2:

               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1   [2]   ?    ?    ?    ?    ?    ?    ?    ?    ?  

    scale-2:        2*2  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?
    scale-3:   3    3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:   5    5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________


Merge will now select 3 as the minimum of 4, 3, and 5:

               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    2    ?    ?    ?    ?    ?    ?    ?    ?    ?  

    scale-2:        4    2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?
    scale-3:  [3]   3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:   5    5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________


and will put it into the next slot in the result stream S, s2:

               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    2   [3]   ?    ?    ?    ?    ?    ?    ?    ?  

    scale-2:        4    2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?
    scale-3:        3*2  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:   5    5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________


Scale-2's head is selected again:

               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    2    3   [4]   ?    ?    ?    ?    ?    ?    ?  

    scale-2:             2*3  2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?
    scale-3:        6    3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:   5    5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________


And then 5 is selected from scale-5 and placed in the result:

               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    2    3    4   [5]   ?    ?    ?    ?    ?    ?  

    scale-2:             6    2*?  2*?  2*?  2*?  2*?  2*?  2*?  2*?
    scale-3:        6    3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:        5*2  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________


Two streams have 6 at their head, both are consumed but only one 6 
is placed in the result:

               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    2    3    4    5   [6]   ?    ?    ?    ?    ?  

    scale-2:                  2*4  2*?  2*?  2*?  2*?  2*?  2*?  2*?
    scale-3:             3*3  3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:        10   5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________


And a few more iterations:

               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    2    3    4    5    6   [8]   ?    ?    ?    ?  

    scale-2:                       2*5  2*?  2*?  2*?  2*?  2*?  2*?
    scale-3:             9    3*?  3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:        10   5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________


               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    2    3    4    5    6    8   [9]   ?    ?    ?  

    scale-2:                       10   2*?  2*?  2*?  2*?  2*?  2*?
    scale-3:                  3*4  3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:        10   5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    _________________________________________________________________


               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    2    3    4    5    6    8    9   [10]  ?    ?  

    scale-2:                            2*6  2*?  2*?  2*?  2*?  2*?
    scale-3:                  12   3*?  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:             5*3  5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________


               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    2    3    4    5    6    8    9    10  [12]  ?  

    scale-2:                                 2*8  2*?  2*?  2*?  2*?
    scale-3:                       3*5  3*?  3*?  3*?  3*?  3*?  3*?
    scale-5:             15   5*?  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    _________________________________________________________________


               s0   s1   s2   s3   s4   s5   s6   s7   s8   s9   s10
           S = 1    2    3    4    5    6    8    9    10   12  [15]

    scale-2:                                 16   2*?  2*?  2*?  2*?
    scale-3:                            3*6  3*?  3*?  3*?  3*?  3*?
    scale-5:                  5*4  5*?  5*?  5*?  5*?  5*?  5*?  5*?
    ________________________________________________________________

所以也许它更像一条蛇,一个头从它的三个尾巴交替咬。

【讨论】:

  • 非常感谢。很高兴知道我不是唯一一个觉得这很难想象的人。很好的答案,再次感谢
  • 它不是一条蛇。其中有两个。每个在这里都称为mergemerge 在小范围内工作:它不知道它的输入来自它之前输出的内容。它只是查看两个输入头,比较它们,拉出最小的一个。这两个合并被安排在一个(最短的)树中,因此较低的输出作为较高位置的另一个的正确输入。如果我将完全遵循本书代码的图表添加到我的答案中,您认为值得吗?
  • 我们确实必须在这里努力“在小事上工作”,“放手并有信心”,就像我们对递归所做的那样:递归的全部意义是不 尝试查看整个机制的工作原理,但专注于较小的部分,确保保留不变量,以便保证将较小的部分组合成一个更大的东西也能正常工作.所以最后两个流作为递归都是关于组合性的。
  • @WillNess 请原谅我简短的 cmets,我既不是快速程序员也不是快速作家,多亏了第 4 章,我的头很痛。我认为我们有共同点 :-) 你很有趣说“放手,相信”,这在书中提到了魔法和咒语(因此封面上的巫师)。我们应该施法而不用担心它们是如何工作的。但是“实现”也在标题中,并且(iirc)这个练习是在我们可以接受流原语的地步,但我们只是被介绍到根据自身定义的流。
  • @ashishmax31 绝对。特别是对于 SICP,当您认为自己“吸取了教训”时,它们会添加细微差别或复杂性,从而暴露您理解上的任何弱点。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-09-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多