【问题标题】:Fastest prime test for small-ish numbers小数的最快质数测试
【发布时间】:2011-04-14 22:08:52
【问题描述】:

我在业余时间玩了 Euler 项目,到了需要进行一些重构的地步。我已经实现了 Miller-Rabin,以及一些筛子。我之前听说过筛子实际上对于较小的数字更快,例如在几百万以下。有人有这方面的信息吗?谷歌不是很有帮助。

【问题讨论】:

  • 随机地,在第 10 题中,我对 root(n) 算法的试用划分让我的米勒-拉宾算法大吃一惊。
  • 为什么不在 trie 中记住以前见过的素数呢?这是一个超级便宜的操作。
  • 你为什么不试试呢?看看我在 Project Euler 中多次使用的简单 Java 筛子的答案:stackoverflow.com/questions/1042902/…

标签: math primes sieve


【解决方案1】:

是的,您会发现大多数算法都可以用空间换取时间。换句话说,通过允许使用更多内存,速度大大提高*a

我实际上并不知道 Miller-Rabin 算法,但除非它比单次左移/添加和内存提取更简单,否则它会被预计算筛。

这里重要的是预先计算好的。就性能而言,预先计算这样的事情是个好主意,因为前一百万个素数在不久的将来不太可能改变:-)

换句话说,使用以下内容创建您的筛子:

unsigned char primeTbl[] = {0,0,1,1,0,1,0,1,0,0,0,1};
#define isPrime(x) ((x < sizeof(primeTbl) ? primeTbl[x] : isPrimeFn(x))

关于不将a++ 之类的东西传递给宏的所有常见警告。这为您提供了两全其美的优势,对“小”素数的快速表查找,对超出范围的素数回退到计算方法。

显然,您会使用其他方法之一编写程序来生成该查找表 - 您真的不想手动输入所有内容。

但是,与所有优化问题一样,衡量,不要猜测!


*a 一个典型的例子是我曾经为嵌入式系统编写的一些三角函数。这是一份有竞争力的合同投标,而且系统的存储空间比 CPU 多一点。

我们实际上赢得了合同,因为我们的功能基准数据击败了竞争对手。

为什么?因为我们将这些值预先计算到了最初在另一台机器上计算的查找表中。通过明智地使用归约(将输入值降低到 90 度以下)和触发属性(余弦只是正弦的相移并且其他三个象限与第一个象限相关的事实),我们将查找表降低到180 个条目(每半度一个)。

最好的解决方案是那些优雅的狡猾的:-)


不管怎样,下面的 C 代码将为您生成这样一个表,所有低于 400 万的素数(其中 283,000 个)。

#include <stdio.h>

static unsigned char primeTbl[4000000];

int main (void) {
    int i, j;

    for (i = 0; i < sizeof(primeTbl); i++)
        primeTbl[i] = 1;

    primeTbl[0] = 0;
    primeTbl[1] = 0;
    for (i = 2; i < sizeof(primeTbl); i++)
        if (primeTbl[i])
            for (j = i + i; j < sizeof(primeTbl); j += i)
                primeTbl[j] = 0;

    printf ("static unsigned char primeTbl[] = {");
    for (i = 0; i < sizeof(primeTbl); i++) {
        if ((i % 50) == 0) {
            printf ("\n   ");
        }
        printf ("%d,", primeTbl[i]);
    }
    printf ("\n};\n");
    printf ("#define isPrime(x) "
        "((x < sizeof(primeTbl) ? primeTbl[x] : isPrimeFn(x))\n");

    return 0;
}

如果您可以将 primeTbl 表增加到 1600 万个条目 (16M),您会发现这足以将素数保持在 100 万以上(前 1,031,130 个素数)。

现在有一些方法可以减少存储空间,例如只存储奇数并调整宏来处理它,或者使用位掩码而不是无符号字符。如果内存可用,我自己更喜欢算法的简单性。

【讨论】:

  • +1 表示“前一百万个素数在不久的将来不太可能改变”,哈哈。我不熟悉 Project Euler 规则,也许这是不允许的?
  • @Mark: Project Euler 没有正式的规则。
  • 是的,如果您愿意将整个 L1 高速缓存 (61 kB) 都摆在桌面上,您可以以非常快的摊销性能检查质数低于 100 万的几率。但对于 Project Euler,您将需要更大范围内的素数,而大数的性能将主导运行时。
  • 您还可以存储从 3 开始的连续素数之间的差异。这允许每个素数 1 个字节用于非常大的范围,但排除了查找。所以检查因素会更快,但检查列表中是否有东西很差。
  • @phkahler:最多可以使用 436,273,009,即 22 MB 的列表。如果您从 5 而不是 3 开始并存储一半的差值,您可以更高,达到 304,599,508,537;这会产生一个 11 GB 的列表。
【解决方案2】:

我建议采用分层方法。首先,确保没有小的质因数。前 20 或 30 个素数的试除法是可行的,但如果您使用巧妙的方法,您可以使用 gcd 减少所需的除法次数。这一步过滤掉了大约 90% 的复合材料。

接下来,测试该数字是否为以 2 为底的强可能素数(Miller-Rabin 检验)。此步骤几乎去除了所有剩余的复合材料,但一些罕见的复合材料可以通过。

最后的证明步骤取决于你想去多大。如果您愿意在小范围内工作,请在 2-pseudoprimes 列表上进行二进制搜索,直到您允许的最大范围内。如果是 2^32,那么您的列表将只有 10,403 个成员,因此查找应该只需要 14 个查询。

如果你想上升到 2^64,现在就足够了(感谢 Jan Feitisma 的工作)检查这个数字是否是 BPSW 伪素数。 (您还可以下载 3 GB 的所有异常列表,删除试用部门将删除的那些,然后编写基于磁盘的二进制搜索。)T. R. Nicely 有一个很好的页面解释了如何合理有效地实现这一点。

如果您需要更高,请实现上述方法并将其用作 Pocklington 式测试的子程序。这延伸了“small-ish”的定义;如果您想了解有关这些方法的更多信息,请询问。

【讨论】:

    【解决方案3】:

    作为预计算概念的一种变体,您可以首先廉价地检查候选数 p 是否可被 2、3、5、7 或 11 整除。如果不是,则声明 p 素数 if 2p-1 = 1 (mod p)。这在某些时候会失败,但由于我测试过它(预计算),所以它的工作量高达 1 亿。

    换句话说,所有以 2 为底的小费马伪素数都可以被 3、5、7 或 11 之一整除。

    编辑:

    正如@starblue 正确指出的那样,以上内容是完全错误的。我的程序中有一个错误。我能做的最好的是将以上内容修改为:

    如果候选 p 可以被 2、3、5、7 或 11 整除,则将其声明为合数;
    否则,如果 p 是 {4181921, 4469471, 5256091, 9006401, 9863461} 之一,则将其声明为复合;
    否则,如果 p 通过了碱基 2 和 5 的 Miller-Rabin 测试,则将其声明为素数;
    否则声明它是复合的。

    我测试了小于 10,000,000 的整数。也许一对不同的碱基会做得更好。

    请接受我对我的错误的道歉。

    编辑 2:

    好吧,我所寻找的信息似乎已经在 Miller-Rabin algorithm 的维基百科页面上,标题为 "Deterministic variants of the test" 的部分。

    【讨论】:

    • @Greg,最后的测试对我来说有点奇怪(1 mod p 对于 p > 1 总是 1)。我猜你的意思是if (2^(p-1) mod p) = 1,是吗?
    • 通过“=”,我的意思是全等的。我应该在 mod p 部分加上括号。已更正。
    • 这似乎是非常有用的信息,以备不时之需 - 它失败的第一个数字是多少?
    • 我还没有找到。也许我会在某个时间通宵运行它,看看我是否击中了一个。
    • +1:新的测试可以达到 14,709,241。基数 2 和 11 相当不错; 5 个例外使它工作到 63,388,033。碱基 2 和 733 略好一些,为 74,927,161。
    【解决方案4】:

    唯一的方法是对自己进行基准测试。当你这样做时,把它写下来,然后在网上发布到某个地方。

    【讨论】:

    • 说真的。你已经完成了实现,为什么不自己计时呢?如果您担心自己可能错过了最快的算法,请将您最好的算法发布为新问题,看看是否有人可以做得更好。
    • 我可以做到这一点,但是我对其中一些测试的实现真的很糟糕。我很确定我写米勒拉宾的方式很糟糕。其实我知道这很糟糕。我想知道最好的情况,所以我可以只处理“正确”的实现,而无需在测试之前将我的每一个都重构为“足够好”。
    • Java 在库中也有 Miller-Rabin,用于 BigInteger。
    猜你喜欢
    • 2011-05-28
    • 2018-03-31
    • 2011-02-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多