【发布时间】:2011-07-03 02:44:21
【问题描述】:
我在考虑一个大数除法的算法:用余数除以 bigint C 除以 bigint D,我们知道 C 在基数 b 中的表示,而 D 的形式为 b^k-1。在示例中显示它可能是最容易的。让我们尝试将 C=21979182173 除以 D=999。
- 我们将数字写成三位数:21 979 182 173
- 我们对连续集合求和(模 999),从左开始:21 001 183 356
- 我们在“超过 999”之前的那些集合中添加 1:22 001 183 356
确实,21979182173/999=22001183 和其余 356。
我已经计算了复杂度,如果我没记错的话,算法应该在 O(n) 中工作,n 是基 b 表示中 C 的位数。我还在 C++ 中做了一个非常粗略和未优化的算法版本(仅适用于 b=10),针对 GMP 的通用整数除法算法对其进行了测试,它确实似乎比 GMP 更好。我在任何地方都找不到类似的实现,所以我不得不求助于对一般除法进行测试。
我发现几篇文章讨论了似乎非常相似的问题,但没有一篇文章专注于实际实现,尤其是在不同于 2 的基数中。我想这是因为数字在内部存储的方式,尽管提到的算法似乎对例如 b=10 很有用,即使考虑到这一点。我也尝试联系其他人,但同样无济于事。
因此,我的问题是:是否有文章或书籍或其他东西描述了上述算法,可能讨论了实现?如果不是,我尝试在 C/C++ 中实现和测试这样的算法是否有意义,或者这个算法在某种程度上是天生不好的?
另外,我不是程序员,虽然我在编程方面还算可以,但我承认我对计算机“内部结构”知之甚少。因此,请原谅我的无知——这篇文章中很可能有一件或多件非常愚蠢的事情。再次抱歉。
非常感谢!
进一步澄清 cmets/answers 中提出的观点:
谢谢大家 - 因为我不想用同样的东西评论所有很棒的答案和建议,所以我只想谈谈你们很多人提到的一点。
我完全清楚,一般来说,以 2^n 为基数显然是最有效的做事方式。几乎所有 bigint 库都使用 2^32 或其他。但是,如果(而且,我强调,它只对这个特定的算法有用!)我们将 bigints 实现为以 b 为底的数字数组,该怎么办?当然,我们在这里要求 b 是“合理的”:b=10,最自然的情况,似乎足够合理。我知道考虑到内存和时间,考虑到数字是如何在内部存储的,我知道它或多或少是低效的,但我已经能够,如果我的(基本的和可能有某种缺陷的)测试是正确的,比 GMP 的一般部门更快地产生结果,这将有助于实现这样的算法。
Ninefingers 注意到在这种情况下我必须使用昂贵的模运算。我希望不会:我可以通过查看 old+new+1 的位数来查看 old+new 是否交叉,例如 999。如果它有 4 位数字,我们就完成了。更重要的是,由于old
当然,我不是在争论这个算法的明显局限性,也不是我声称它不能改进——它只能除以特定类别的数字,我们必须先验地知道以 b 为基数的被除数的表示.但是,例如,对于 b=10,后者似乎很自然。
现在,假设我们已经实现了我上面概述的 bignums。说以 b 为底的 C=(a_1a_2...a_n) 和 D=b^k-1。该算法(可能会更加优化)会像这样。希望不要有太多错别字。
- 如果 k>n,我们显然已经完成了
- 在 C 的开头添加一个零(即 a_0=0)(以防我们尝试将 9999 除以 99)
- l=n%k (“常规”整数的 mod - 不应该太贵)
- old=(a_0...a_l) (第一组数字,可能少于k个数字)
- for (i=l+1; i (我们将有 floor(n/k) 次左右的迭代)
- new=(a_i...a_(i+k-1))
- new=new+old (这是 bigint 加法,因此 O(k))
- aux=new+1 (再次,bigint 加法 - O(k) - 我不满意)
- 如果 aux 有超过 k 位
- 删除辅助的第一个数字
- old=old+1 (再次添加 bigint)
- old 在开头用零填充,这样它就有尽可能多的数字
- (a_(ik)...a_(i-1))=old (如果 i=l+1, (a _ 0...a _ l)=旧)
- 新=辅助
- 在新的开头用零填充,这样它就有尽可能多的数字
- (a_i...a_(i+k-1)=新
- quot=(a_0...a_(n-k+1))
- rem=新
感谢您与我讨论这个问题 - 正如我所说,如果没有人发现它有任何致命缺陷,我认为这确实是一个有趣的“特殊情况”算法,可以尝试实施、测试和讨论。如果这是迄今为止尚未广泛讨论的事情,那就更好了。请让我知道你在想什么。抱歉,帖子太长了。
另外,还有几个个人的 cmets:
@Ninefingers:我实际上对 GMP 的工作原理、它的作用以及一般的 bigint 除法算法有一些(非常基本的!)知识,所以我能够理解你的大部分论点。我也知道 GMP 是高度优化的,并且在某种程度上为不同的平台定制了自己,所以我当然不会试图“打败它”——这似乎与用尖棍攻击坦克一样富有成效。然而,这不是这个算法的想法——它适用于非常特殊的情况(GMP 似乎没有涵盖)。在不相关的说明中,您确定在 O(n) 中完成了一般划分吗?我见过的最多的是M(n)。 (如果我理解正确,那在实践中(Schönhage–Strassen 等)可能不会达到 O(n)。Fürer 的算法仍然没有达到 O(n),如果我是正确的,几乎纯粹是理论。)
@Avi Berger:这实际上似乎与“抛出九”并不完全相同,尽管想法相似。但是,如果我没记错的话,上述算法应该一直有效。
【问题讨论】:
-
那么您是否建议将所有整数存储在 BCD 中以加快除法速度?将 base-2 转换为 base-10 涉及整数除法,不是吗? :-)
-
有趣的算法,虽然实际应用可能有限。从技术上讲,选择不同的基数将允许您将其与任意除数一起使用,但诀窍是首先将其转换为该基数。
-
@Ninefingers:编辑似乎有点长。我试图删除它,但似乎版主已经这样做了。还是谢谢。
-
re:使用 base-
b的四肢而不是始终以 2^32 为底:如果您需要经常除以一些b,这是一个有效的选项。例如,为了在代码高尔夫挑战(使用 perf req)中打印 Fibonacci(10^9) 的前 1000 位数字,我使用了一种半暴力方法,只保留 1009 个最重要的 decimal 数字过大时除以 10^9。以 10^9 为基数的分支(在 32 位元素中)使得这非常有效,值得手动执行并比较添加。 105 bytes of x86 machine code
标签: c++ algorithm performance integer-division bigint