【问题标题】:How can natural numbers be represented to offer constant time addition?如何表示自然数以提供恒定时间加法?
【发布时间】:2015-02-25 21:23:02
【问题描述】:

Cirdec's answer 回答了一个基本上不相关的问题,这让我想知道如何最好地表示具有恒定时间加法、减一和零测试的自然数。

为什么 Peano 算术不够好:

假设我们使用

data Nat = Z | S Nat

那么我们可以写

Z + n = n
S m + n = S(m+n)

我们可以在 O(1) 时间内计算 m+n,方法是将 m-r 借方(对于某些常量 r),每个 S 构造函数上添加一个到 n。要获得 O(1) isZero,我们需要确保每个 S 构造函数最多有 p 借方,对于一些常量 p。如果我们计算a + (b + (c+...)),这很有效,但如果我们计算((...+b)+c)+d,它就会崩溃。问题是借方在前端堆积。

一个选项

简单的方法是直接使用可连接列表,例如 Okasaki 描述的那些。有两个问题:

  1. O(n) 空间并不理想。

  2. 当我们不关心列表的顺序时,引导队列的复杂性是必要的(至少对我而言)并不完全清楚。

【问题讨论】:

  • O(n) 中的n 是什么?数字的大小或输入的长度? Okasaki 描述的“可连接列表”是什么?
  • @Cirdec,如果无法改善空间使用率,我不会感到惊讶,现在我想了更多。 O(1) 加法是一个非常严格的要求!
  • 你不能得到恒定的时间加法。如果你限制数字的大小,你只能得到恒定的时间。
  • @augustss,怎么样? Okasaki 的可链表支持 O(1) 追加和 O(1) 递减。
  • 并不是说这对 SO 不合适,但您也可以考虑compsci

标签: haskell math data-structures


【解决方案1】:

据我所知,Idris(一种非常接近 Haskell 的依赖类型纯函数式语言)以一种非常直接的方式处理了这个问题。编译器知道Nats 和Fins(上限Nats)并尽可能用机器整数类型和操作替换它们,因此生成的代码非常有效。但是,对于自定义类型(甚至是同构类型)以及编译阶段而言,情况并非如此(有一些代码示例使用 Nats 进行类型检查,导致编译时间呈指数增长,如果需要,我可以提供它们) .

对于 Haskell,我认为可以实现类似的编译器扩展。另一种可能性是制作可以转换代码的 TH 宏。当然,这两种选择都不容易。

【讨论】:

  • 它被热切地评估,所以无论类型系统多么相似,它都不会那么相似。有很多样式在懒惰的环境中非常有效,而在急切的环境中则很糟糕,反之亦然。
  • Idris naturals 一旦超过机器字,就只提供对数时间运算!
  • 确实,机器整数类型是对数的,而不是 O(1)。对于“足够小”的数字(适合机器字),它们似乎只是常数时间。
【解决方案2】:

我的理解是,在基本的计算机编程术语中,潜在的问题是您希望在恒定时间内连接列表。列表没有前向引用之类的作弊,例如,您不能在 O(1) 时间内跳到末尾。

您可以改用环,无论使用a+(b+(c+...)) 还是((...+c)+b)+a 逻辑,都可以在O(1) 时间内合并。环中的节点不需要双重链接,只需链接到下一个节点即可。

减法是删除任何节点,O(1),测试零(或一)是微不足道的。然而,对 n > 1 的测试是 O(n)。

如果您想减少空间,那么在每个操作中,您可以合并插入或删除点的节点,并将剩余的权重更高。您执行的操作越多,表示就越紧凑!不过,我认为最坏的情况仍然是 O(n)。

【讨论】:

  • 问题描述中的n是数字的大小,而不是位数。我很清楚标准的二进制加法是 O(log m+log n)。这个问题根本不是在谈论那种形式的加法。
  • 谢谢,已根据反馈修改。
  • 能否更具体地了解您提出的数据结构?问题是关于不可变的功能数据结构,因此特别是不允许使用可变指针。另外,你能扩展一下加权节点吗?通常你会使用数字来表示权重,但当然我们在这里尝试表示数字,假设我们还没有数字。
【解决方案3】:

我们知道自然数的有效加法有两种“极值”解决方案:

  1. 内存效率高,自然数的标准二进制表示,使用 O(log n) 内存并需要 O(log n) 时间进行加法。 (另请参阅the Okasaki's book 中的“二进制表示”一章。)
  2. CPU 效率高,仅使用 O(1) 时间。 (请参阅本书中的“结构抽象”一章。)但是,该解决方案使用 O(n) 内存,因为我们将自然数 n 表示为 n()

    我还没有进行实际计算,但我相信对于 O(1) 数值加法,我们不需要 O(1) 的全部功能FIFO 队列,以同样的方式引导标准列表 [] (LIFO) 就足够了。如果您有兴趣,我可以尝试详细说明。

CPU 高效解决方案的问题是我们需要在内存表示中添加一些冗余,以便我们可以腾出足够的 CPU 时间。在某些情况下,可以在不影响内存大小的情况下添加这样的冗余(例如 O(1) 递增/递减操作)。如果我们允许任意树形状,例如在具有引导列表的 CPU 高效解决方案中,只需在 O(log n) 内存中使用too many tree shapes 来区分它们。

所以问题是:我们能否找到恰到好处的冗余量,这样亚线性内存量就足够了,并且我们可以实现O(1) 添加?我相信答案是

让我们有一个具有O(1)时间加法的表示+算法。然后让我们有一个 m 位大小的数字,我们将其计算为 2^k 个数字的总和,每个数字的大小为 ( mk)-位。为了表示我们需要(不管表示如何)最小 (mk) 位内存的每个和,所以在开始时,我们从(至少)(mk) 2^ k 位内存。现在,在每次添加 2^k 时,我们都可以执行恒定数量的操作,因此我们能够处理(理想情况下删除)总共 C 2^k em> 位。因此,最后,我们需要表示结果的位数的下限是 (m-k-C) 2^k 位。由于 k 可以任意选择,我们的对手可以设置 k=mC-1,这意味着总和将至少用 2^(mC- 1) = 2^m/2^(C+1) ∈ O(2^m) 位。所以自然数 n 总是需要 O(n) 位内存!

【讨论】:

  • @dfeuer 因为每个都是(mk)位的量级,所以有2^(mk)个这样的数字,所以我们需要(mk) 位信息来区分它们——我们无法超越。我们可以使用某种压缩并为某些数字使用较少的实际位,但是对于其他数字我们需要更多的位。由于这需要对任何总和保持不变,obviously 对手可以选择具有 (m-k) 位或更多位的和数。
猜你喜欢
  • 2015-08-10
  • 2018-12-16
  • 2013-11-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多