如何导出函数段?

发布于 2024-12-25 23:44:00 字数 253 浏览 4 评论 0原文

如何构造函数 segs 返回列表中所有连续段的列表? 例如,(segs '(list))应该产生以下答案:

(() (t) (s) (s t) (i) (i s) (i s t) (l) (l i) (l i s) (l i s t))

我对如何按照HtDP中描述的设计原则解决这个问题特别感兴趣(不,这不是书上的问题,欢迎大家讨论!)如何解决?程序推导时应遵循哪些原则?

How can I construct the function segs that returns a list of all contiguous segments in a list?
For example, (segs '(l i s t)) should produce the following answer:

(() (t) (s) (s t) (i) (i s) (i s t) (l) (l i) (l i s) (l i s t))

I'm especially interested in how to solve this problem in accordance with the design principles described in HtDP (no, this is not the problem from the book, so please feel free to discuss it!) How to solve it? Which principles to use in program derivation?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

╭⌒浅淡时光〆 2025-01-01 23:44:00

首先构建一组相关的示例,首先是最简单的:

(equal? (segs '())
        (list '()))
(equal? (segs '(z))
        (list '()
              '(z)))
(equal? (segs '(y z))
        (list '() '(z)
              '(y) '(y z)))
(equal? (segs '(x y z))
        (list '() '(z) '(y) '(y z)
              '(x) '(x y) '(x y z)))

通过查看示例,您可以进行观察(我使用格式来突出显示):每个示例的答案都包含答案中的所有元素对于前面的例子。事实上,非空列表的连续子序列只是其尾部的连续子序列加上列表本身的非空前缀。

因此,将 main 函数搁置起来,编写 non-empty-prefixes

non-empty-prefixes : list -> (listof non-empty-list)

有了该辅助函数,就可以轻松编写 main 函数。

(可选)生成的朴素函数的复杂性很差,因为它重复调用非空前缀。考虑(segs (cons head tail))。它调用 (non-empty-prefixes tail) 两次:一次是因为它调用 (segs tail) ,后者又调用 (non-empty-prefixes tail) >,再次因为它调用 (non-empty-prefixes (cons head tail)) ,它递归地调用 (non-empty-prefixes tail) 。这意味着朴素函数具有不必要的复杂性。

问题是 (segs tail) 计算 (non-empty-prefixes tail) 然后忘记它,所以 (segs (cons head tail)) code> 必须重做工作。解决方案是通过将 segsnon-empty-prefixes 融合到计算两个答案的单个函数中来保留这些额外信息:

segs+ne-prefixes : list -> (values (listof list) (listof non-empty-list))

然后定义 segs 作为适配器函数,只删除第二部分。这解决了复杂性的主要问题。

(编辑添加)关于segs+ne-prefixes:这是定义非空前缀的一种方法。 (注意:空列表没有非空前缀。不需要引发错误。)

;; non-empty-prefixes : list -> (listof non-empty-list)
(define (non-empty-prefixes lst)
  (cond [(empty? lst)
         empty]
        [(cons? lst)
         (map (lambda (p) (cons (first lst) p))
              (cons '() (non-empty-prefixes (rest lst))))]))

并且 segs 看起来像这样:

;; segs : list -> (listof list)
(define (segs lst)
  (cond [(empty? lst) (list '())]
        [(cons? lst)
         (append (segs (rest lst))
                 (non-empty-prefixes lst))]))

您可以像这样融合它们:

;; segs+ne-prefixes : list -> (values (listof list) (listof non-empty-list))
;; Return both the contiguous subsequences and the non-empty prefixes of lst
(define (segs+ne-prefixes lst)
   (cond [(empty? lst)
          ;; Just give the base cases of each function, together
          (values (list '())
                  empty)]
         [(cons? lst)
          (let-values ([(segs-of-rest ne-prefixes-of-rest)
                        ;; Do the recursion on combined function once!
                        (segs+ne-prefixes (rest lst))])
            (let ([ne-prefixes
                   ;; Here's the body of the non-empty-prefixes function
                   ;; (the cons? case)
                   (map (lambda (p) (cons (first lst) p))
                        (cons '() ne-prefixes-of-rest))])
              (values (append segs-of-rest ne-prefixes)
                      ne-prefixes)))]))

这个函数仍然遵循设计配方(或如果我展示了我的测试,它会的):特别是,它使用列表上的结构递归模板。 HtDP 不讨论 valueslet-values,但您可以使用辅助结构来对信息进行分组,从而完成相同的操作。

HtDP 稍微讨论了复杂性,但这种计算重组通常在算法课程的“动态编程和记忆”下更多地讨论。请注意,融合这两个函数的另一种方法是记忆非空前缀;这也可以解决复杂性问题。

最后一件事:靠近末尾的 append 的参数应该反转为 (append ne-prefixes segs-of-rest)。 (当然,这意味着重写所有测试以使用新顺序,或者编写/查找顺序不敏感的列表比较函数。)尝试在大型列表(大约 300-400 个元素)上对函数的两个版本进行基准测试,看看你是否能分辨出差异,并看看你是否能解释它。 (这更多是算法材料,而不是设计。)

Start by building up a set of related examples, most trivial first:

(equal? (segs '())
        (list '()))
(equal? (segs '(z))
        (list '()
              '(z)))
(equal? (segs '(y z))
        (list '() '(z)
              '(y) '(y z)))
(equal? (segs '(x y z))
        (list '() '(z) '(y) '(y z)
              '(x) '(x y) '(x y z)))

By looking at the examples you can make an observation (which I've used the formatting to highlight): the answer for each example includes the all of the elements from the answer for the previous example. In fact, the contiguous subsequences of a non-empty list are just the contiguous subsequences of its tail together with the non-empty prefixes of the list itself.

So put the main function on hold and write non-empty-prefixes

non-empty-prefixes : list -> (listof non-empty-list)

With that helper function, it's easy to write the main function.

(Optional) The resulting naive function has bad complexity, because it repeats calls to non-empty-prefixes. Consider (segs (cons head tail)). It calls (non-empty-prefixes tail) twice: once because it calls (segs tail) which calls (non-empty-prefixes tail), and again because it calls (non-empty-prefixes (cons head tail)) which calls (non-empty-prefixes tail) recursively. That means the naive function has unnecessarily bad complexity.

The problem is that (segs tail) computes (non-empty-prefixes tail) and then forgets it, so (segs (cons head tail)) has to redo the work. The solution is to hold on to that extra information by fusing segs and non-empty-prefixes into a single function that computes both answers:

segs+ne-prefixes : list -> (values (listof list) (listof non-empty-list))

Then define segs as an adapter function that just drops the second part. That fixes the main issue with the complexity.

(Edited to add) Regarding segs+ne-prefixes: here's one way to define non-empty-prefixes. (Note: the empty list has no non-empty prefixes. No need to raise an error.)

;; non-empty-prefixes : list -> (listof non-empty-list)
(define (non-empty-prefixes lst)
  (cond [(empty? lst)
         empty]
        [(cons? lst)
         (map (lambda (p) (cons (first lst) p))
              (cons '() (non-empty-prefixes (rest lst))))]))

And segs looks like this:

;; segs : list -> (listof list)
(define (segs lst)
  (cond [(empty? lst) (list '())]
        [(cons? lst)
         (append (segs (rest lst))
                 (non-empty-prefixes lst))]))

You can fuse them like this:

;; segs+ne-prefixes : list -> (values (listof list) (listof non-empty-list))
;; Return both the contiguous subsequences and the non-empty prefixes of lst
(define (segs+ne-prefixes lst)
   (cond [(empty? lst)
          ;; Just give the base cases of each function, together
          (values (list '())
                  empty)]
         [(cons? lst)
          (let-values ([(segs-of-rest ne-prefixes-of-rest)
                        ;; Do the recursion on combined function once!
                        (segs+ne-prefixes (rest lst))])
            (let ([ne-prefixes
                   ;; Here's the body of the non-empty-prefixes function
                   ;; (the cons? case)
                   (map (lambda (p) (cons (first lst) p))
                        (cons '() ne-prefixes-of-rest))])
              (values (append segs-of-rest ne-prefixes)
                      ne-prefixes)))]))

This function still follows the design recipe (or it would, if I had shown my tests): in particular, it uses the template for structural recursion on a list. HtDP doesn't talk about values and let-values, but you could do the same thing with an auxiliary structure to group the information.

HtDP talks about complexity a little bit, but this sort of regrouping of computation is usually discussed more in an algorithms course, under "dynamic programming and memoization". Note that an alternative to fusing the two functions would have been to memoize non-empty-prefixes; that also would have fixed the complexity.

One last thing: the arguments to append near the end should be reversed, to (append ne-prefixes segs-of-rest). (Of course, that means rewriting all of the tests to use the new order, or writing/finding an order-insensitive list comparison function.) Try benchmarking the two versions of the function on a large list (around 300-400 elements), see if you can tell a difference, and see if you can explain it. (This is more algorithms material, not design.)

極樂鬼 2025-01-01 23:44:00

正在进行两次递归:第一次从左侧砍掉原子,第二次从右侧砍掉原子。下面是我如何用两个函数递归地解决这个问题,用简单的术语来说(因为我不太熟悉Scheme):

Start with the FullList variable in function A, 
accumulate right-chop results on FullList from recursive function B, 
then chop off the left character of FullList and call function A with it. 
Stop when an empty list is received. 

累积所有结果,你就很优秀了。

There's 2 recursions going on: the first chops atoms off the left, and the second chops atoms off the right. Here's how I'd solve it recursively in 2 functions, in plain terms (since I'm not fluent in Scheme):

Start with the FullList variable in function A, 
accumulate right-chop results on FullList from recursive function B, 
then chop off the left character of FullList and call function A with it. 
Stop when an empty list is received. 

Accumulate all results, and you're golden.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文