【问题标题】:Prolog Accumulators. Are they really a "different" concept?Prolog 累加器。它们真的是一个“不同”的概念吗?
【发布时间】:2013-11-25 12:49:10
【问题描述】:

我在我的人工智能实验室学习 Prolog,来自 Learn Prolog Now!

在第 5 章中,我们将了解累加器。并且作为一个例子,给出了这两个代码sn-ps。 查找列表的长度

没有累加器

len([],0).
len([_|T],N) :- len(T,X), N is X+1.

带累加器

accLen([_|T],A,L) :- Anew is A+1, accLen(T,Anew,L).
accLen([],A,A).

我无法理解,这两个 sn-ps 在概念上有何不同?累加器到底有什么不同?有什么好处?

累加器听起来像中间变量。 (如果我错了,请纠正我。)到目前为止我已经在我的程序中使用它们,所以它真的有那么大的概念吗?

【问题讨论】:

  • 正如其他人以各种方式指出的那样,使用累加器为您提供了一个使用 尾递归 的解决方案,该解决方案具有优化优势。例如,请参阅此处的讨论:stackoverflow.com/questions/14096656/…
  • @false 当我引用文献时,我顺其自然。就个人而言,我也会在规则之前写事实......

标签: recursion prolog tail-recursion accumulator


【解决方案1】:

TL;DR:是的。

假设你要从左边的城市A到右边的城市B,你想提前知道两者之间的距离。你是如何做到这一点的?

mathematician 在这样的位置使用magic,称为structural recursion。他对自己说,如果我将自己的副本发送到城市B更近一步,并询问会怎么样它到城市的距离?然后,我将在其结果中添加 1,从我的副本中接收到它,因为我已经将它 一个 发送了一步 closer em> 走向城市,不动一寸就知道我的答案!当然,如果我已经在城门口了,我不会把我的任何副本发送到任何地方,因为我知道距离是 0 - 没有移动一英寸!

我怎么知道我的复制我的会成功?仅仅是因为他会遵循相同的确切规则,同时从离我们目的地更近的一点开始。无论我的答案是什么价值,他的都会少一个,并且只有有限数量的我们的副本会被调用——因为城市之间的距离是有限的。所以整个操作肯定会在有限的时间内完成,我得到我的答案。因为在无限时间过去之后得到你的答案,根本就没有得到它 - 永远。

现在,在提前找到他的答案后,我们谨慎的魔术师数学家已准备好踏上他安全(现在!)的旅程。

但这当然根本不是魔术——这都是一个肮脏的把戏!他不是凭空提前找到答案的——他已经派出整个堆栈其他人来为他找到答案。繁重的工作终究是要完成的,他只是装作不知道而已。 走过的距离。而且,back的距离也必须经过,每个副本都必须将结果告诉他们的主人,结果实际上是在back的路上创建的em> 从目的地。这一切都发生在我们的假魔术师开始自己走路之前。 团队合作如何。对于来说,这似乎是一笔甜蜜的交易。但总的来说...


这就是magician mathematician 的想法。但他的dual 勇敢的旅行者只是goes on a journey,并沿途计算他的步数,每一步,之前当前步数计数器上加 1 他的实际旅程的其余部分。没有任何伪装了。旅程可能是有限的,也可能是无限的——他无法预先知道。但是在他的路线上的每一点,因此当他也到达城市B的大门时,他会知道他走了这么远的距离。而且他当然不用一路回到路的起点告诉自己结果。

这就是第一个的结构递归和第二个使用的 tail recursion with accumulatortail recursion modulo conscorecursion 之间的区别。第一个知识建立在目标返回的路上;第二个 - 在从起点向前目标的路上。 旅程是目的地。

另见:


你问这一切的实际意义是什么?为什么,想象一下我们的朋友魔术师数学家需要煮一些鸡蛋。他有一个锅;一个水龙头;热板;和一些鸡蛋。他要做什么?

嗯,这很简单——他只需将鸡蛋放入锅中,从水龙头中加入一些水,然后将其放在电热板上。

如果他已经给了一个装有鸡蛋和水的锅怎么办?为什么,这对他来说更容易——他只需把鸡蛋拿出来,倒出水,最后就会遇到他已经知道如何解决的问题!纯粹的魔法,不是吗!

在我们嘲笑这个可怜的家伙之前,we mustn't forgetthe tale of the centipede。有时无知幸福。但是当所需的知识很简单,像这里的距离一样“一维”时,假装完全没有记忆是犯罪的。

【讨论】:

    【解决方案2】:

    当您给某物起一个名称时,它会突然变得比以前更加真实。现在可以通过简单地使用概念名称来讨论某些事情。没有更多的哲学,不,累加器没有什么特殊,但它们很有用。

    在实践中,遍历没有累加器的列表:

    foo([]).
    foo([H|T]) :-
        foo(T).
    

    列表的头部被留下,递归调用无法访问。在每个递归级别,您只能看到列表的剩余部分。

    使用累加器:

    bar([], _Acc).
    bar([H|T], Acc) :-
        bar(T, [H|Acc]).
    

    在每一个递归步骤中,你都有剩余的列表你经历过的所有元素。在您的 len/3 示例中,您只保留计数,而不是实际元素,因为这就是您所需要的。

    一些谓词(如len/3)可以使用累加器进行尾递归:您无需等待输入结束(用尽列表中的所有元素)来完成实际工作,而是执行它当您获得输入时逐渐增加。 Prolog 不必将值留在堆栈中,并且可以为您进行尾调用优化。

    需要知道“到目前为止的路径”的搜索算法(或任何需要具有状态的算法)使用相同技术的更通用形式,通过为递归调用提供“中间结果”。例如,一个行程编码器可以定义为:

    rle([], []).
    rle([First|Rest],Encoded):- 
        rle_1(Rest, First, 1, Encoded).               
    
    rle_1([], Last, N, [Last-N]).
    rle_1([H|T], Prev, N, Encoded) :-
        (   dif(H, Prev) 
        ->  Encoded = [Prev-N|Rest],
            rle_1(T, H, 1, Rest)
        ;   succ(N, N1),
            rle_1(T, H, N1, Encoded)
        ).
    

    希望对您有所帮助。

    【讨论】:

    • 接受的答案很好,但我认为这个要好得多。
    • 用户@boris的另一个好答案。
    【解决方案3】:

    累加器中间变量, Prolog中一个重要的(阅读基本)主题,因为它允许反转一些基本算法的信息流,对程序的效率有重要影响。

    以反转列表为例

    nrev([],[]).
    nrev([H|T], R) :- nrev(T, S), append(S, [H], R).
    
    rev(L, R) :- rev(L, [], R).
    rev([], R, R).
    rev([H|T], C, R) :- rev(T, [H|C], R).
    

    nrev/2 (naive reverse) 它是 O(N^2),其中 N 是列表长度,而 rev/2 它是 O(N)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-09-04
      • 2016-07-16
      • 2018-02-02
      • 2018-03-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多