给定素数N,计算下一个素数?


67

一位同事刚刚告诉我,由于与哈希相关的不可思议的原因,C#词典集合将按质数调整大小。我的直接问题是:“它怎么知道下一个素数?它们是在大型表中还是在运行中进行计算?这是在插入时导致确定大小的可怕的不确定性运行时”

所以我的问题是,给定N(一个质数),计算下一个质数的最有效方法是什么?


3
这确实属于mathoverflow。
柯克·布罗德赫斯特

2
也许您的同事是不正确的,或者它使用了一些预先计算的素数而不是找到下一个素数。
总统詹姆斯·波尔克(James K. Polk)2010年

20
@柯克:我不同意-这是一个算法问题,而不是数学问题。
吉姆·刘易斯

4
@Kirk所有这些都属于理论计算机科学,这在编程和数学中间处于非常重要的地位。因此,老实说,我在这两个网站上都看不到这个问题。
moinudin

11
@Kirk:这绝对不属于MathOverflow,它仅适用于研究级问题。我也不同意它必须位于math.stackexchange.com上,但至少也适合在那里。
Antal Spector-Zabusky 2010年

Answers:


34

已知连续质数之间间隙非常小,对于质数370261,第一个超过100的间隙发生。这意味着即使简单的蛮力在大多数情况下也足够快,取O(ln(p)* sqrt(p))。

对于p = 10,000,这是O(921)运算。请记住,我们将在每个O(ln(p))插入后执行一次(粗略地说),这完全在大多数问题的限制内,在大多数现代硬件上,这些问题的发生时间约为毫秒。


在增加字典的背景下,我不会称其为“快速”。
总统詹姆斯·波尔克(James K. Polk)2010年

同意复杂性并不高,但是每个操作都是相对较重的剩余检查;&随着p的增加,支票本身的复杂性也随之增加。
柯克·布罗德赫斯特

@GregS参见我的编辑。@Kirk可以肯定的是,逐渐意识到这些费用是使一名经验丰富的程序员的其中一件事情。
moinudin

@marcog除非我仍在睡觉,否则对于p = 10000,ln(p)= 9.2和sqrt(p)= 100,=> O(920)。
Kirk Broadhurst,2010年

@柯克不,是我那个熟睡的人。定影!
moinudin

76

大约一年前,我在为c ++ 11实现无序(哈希)容器时为libc ++工作。我以为我会在这里分享我的经验。这种经验支持marcog对“蛮力”的 合理定义的公认答案

这意味着在大多数情况下,即使是简单的蛮力也足够快,平均取O(ln(p)* sqrt(p))。

我开发了几种实现size_t next_prime(size_t n)此功能的规范的实现:

返回: 大于或等于的最小素数n

的每个实现next_prime都带有辅助功能is_primeis_prime应被视为私人实施细节;不能直接由客户调用。这些实现中的每一个当然都经过了正确性测试,但还通过以下性能测试进行了测试:

int main()
{
    typedef std::chrono::high_resolution_clock Clock;
    typedef std::chrono::duration<double, std::milli> ms;
    Clock::time_point t0 = Clock::now();

    std::size_t n = 100000000;
    std::size_t e = 100000;
    for (std::size_t i = 0; i < e; ++i)
        n = next_prime(n+1);

    Clock::time_point t1 = Clock::now();
    std::cout << e/ms(t1-t0).count() << " primes/millisecond\n";
    return n;
}

我应该强调,这是一项性能测试,并不反映典型的用法,看起来更像是:

// Overflow checking not shown for clarity purposes
n = next_prime(2*n + 1);

所有性能测试均使用以下命令进行编译:

clang++ -stdlib=libc++ -O3 main.cpp

实施1

有七个实现。显示第一个实现的目的是证明,如果您没有停止测试候选素数x的因子,sqrt(x)那么您甚至都无法实现可以归为蛮力的实现。此实现 速度很慢

bool
is_prime(std::size_t x)
{
    if (x < 2)
        return false;
    for (std::size_t i = 2; i < x; ++i)
    {
        if (x % i == 0)
            return false;
    }
    return true;
}

std::size_t
next_prime(std::size_t x)
{
    for (; !is_prime(x); ++x)
        ;
    return x;
}

对于此实现e,仅为了获得合理的运行时间,我必须将其设置为100而不是100000:

0.0015282 primes/millisecond

实施2

此实现是蛮力实现中最慢的,并且与实现1的唯一区别是,当因子超过时,它将停止测试素数sqrt(x)

bool
is_prime(std::size_t x)
{
    if (x < 2)
        return false;
    for (std::size_t i = 2; true; ++i)
    {
        std::size_t q = x / i;
        if (q < i)
            return true;
        if (x % i == 0)
            return false;
    }
    return true;
}

std::size_t
next_prime(std::size_t x)
{
    for (; !is_prime(x); ++x)
        ;
    return x;
}

请注意,sqrt(x)它不是直接计算出来的,而是由推断出来的q < i。这样可以使速度加快数千倍:

5.98576 primes/millisecond

并验证了marcog的预测:

...这完全在大多数问题的约束范围内,而大多数现代硬件上的问题大约需要毫秒。

实施3

通过避免使用%运算符,可以使速度提高近一倍(至少在我使用的硬件上):

bool
is_prime(std::size_t x)
{
    if (x < 2)
        return false;
    for (std::size_t i = 2; true; ++i)
    {
        std::size_t q = x / i;
        if (q < i)
            return true;
        if (x == q * i)
            return false;
    }
    return true;
}

std::size_t
next_prime(std::size_t x)
{
    for (; !is_prime(x); ++x)
        ;
    return x;
}

11.0512 primes/millisecond

实施4

到目前为止,我什至没有使用常识,即2是唯一的偶数素数。此实现结合了这些知识,速度几乎翻了一番:

bool
is_prime(std::size_t x)
{
    for (std::size_t i = 3; true; i += 2)
    {
        std::size_t q = x / i;
        if (q < i)
            return true;
        if (x == q * i)
            return false;
    }
    return true;
}

std::size_t
next_prime(std::size_t x)
{
    if (x <= 2)
        return 2;
    if (!(x & 1))
        ++x;
    for (; !is_prime(x); x += 2)
        ;
    return x;
}

21.9846 primes/millisecond

实施4可能是大多数人认为“蛮力”的想法。

实施5

使用以下公式,您可以轻松选择所有不能被2或3整除的数字。

6 * k + {1, 5}

其中k> =1。以下实现使用此公式,但使用可爱的xor技巧实现:

bool
is_prime(std::size_t x)
{
    std::size_t o = 4;
    for (std::size_t i = 5; true; i += o)
    {
        std::size_t q = x / i;
        if (q < i)
            return true;
        if (x == q * i)
            return false;
        o ^= 6;
    }
    return true;
}

std::size_t
next_prime(std::size_t x)
{
    switch (x)
    {
    case 0:
    case 1:
    case 2:
        return 2;
    case 3:
        return 3;
    case 4:
    case 5:
        return 5;
    }
    std::size_t k = x / 6;
    std::size_t i = x - 6 * k;
    std::size_t o = i < 2 ? 1 : 5;
    x = 6 * k + o;
    for (i = (3 + o) / 2; !is_prime(x); x += i)
        i ^= 6;
    return x;
}

这实际上意味着该算法只需要检查1/3的整数的素数,而不是数字的1/2,性能测试表明预期的速度提高了近50%:

32.6167 primes/millisecond

实施6

此实现是实现5的逻辑扩展:它使用以下公式计算不能被2、3和5整除的所有数字:

30 * k + {1, 7, 11, 13, 17, 19, 23, 29}

它还展开is_prime内的内部循环,并创建一个“小素数”列表,该列表对于处理小于30的数字很有用。

static const std::size_t small_primes[] =
{
    2,
    3,
    5,
    7,
    11,
    13,
    17,
    19,
    23,
    29
};

static const std::size_t indices[] =
{
    1,
    7,
    11,
    13,
    17,
    19,
    23,
    29
};

bool
is_prime(std::size_t x)
{
    const size_t N = sizeof(small_primes) / sizeof(small_primes[0]);
    for (std::size_t i = 3; i < N; ++i)
    {
        const std::size_t p = small_primes[i];
        const std::size_t q = x / p;
        if (q < p)
            return true;
        if (x == q * p)
            return false;
    }
    for (std::size_t i = 31; true;)
    {
        std::size_t q = x / i;
        if (q < i)
            return true;
        if (x == q * i)
            return false;
        i += 6;

        q = x / i;
        if (q < i)
            return true;
        if (x == q * i)
            return false;
        i += 4;

        q = x / i;
        if (q < i)
            return true;
        if (x == q * i)
            return false;
        i += 2;

        q = x / i;
        if (q < i)
            return true;
        if (x == q * i)
            return false;
        i += 4;

        q = x / i;
        if (q < i)
            return true;
        if (x == q * i)
            return false;
        i += 2;

        q = x / i;
        if (q < i)
            return true;
        if (x == q * i)
            return false;
        i += 4;

        q = x / i;
        if (q < i)
            return true;
        if (x == q * i)
            return false;
        i += 6;

        q = x / i;
        if (q < i)
            return true;
        if (x == q * i)
            return false;
        i += 2;
    }
    return true;
}

std::size_t
next_prime(std::size_t n)
{
    const size_t L = 30;
    const size_t N = sizeof(small_primes) / sizeof(small_primes[0]);
    // If n is small enough, search in small_primes
    if (n <= small_primes[N-1])
        return *std::lower_bound(small_primes, small_primes + N, n);
    // Else n > largest small_primes
    // Start searching list of potential primes: L * k0 + indices[in]
    const size_t M = sizeof(indices) / sizeof(indices[0]);
    // Select first potential prime >= n
    //   Known a-priori n >= L
    size_t k0 = n / L;
    size_t in = std::lower_bound(indices, indices + M, n - k0 * L) - indices;
    n = L * k0 + indices[in];
    while (!is_prime(n))
    {
        if (++in == M)
        {
            ++k0;
            in = 0;
        }
        n = L * k0 + indices[in];
    }
    return n;
}

可以说,这超出了“蛮力”,并且有利于将速度再提高27.5%,从而达到:

41.6026 primes/millisecond

实施7

反复玩上面的游戏是很实际的,为不能被2、3、5和7整除的数字建立一个公式:

210 * k + {1, 11, ...},

源代码未在此处显示,但与实现6非常相似。这是我选择实际用于libc ++的无序容器的实现,并且该源代码是开源的(可在链接中找到)。

最后的迭代有利于将速度进一步提高14.6%,以:

47.685 primes/millisecond

使用此算法可确保libc ++哈希表的客户端可以选择自己决定的任何素数,这对他们的情况最有利,并且此应用程序的性能是可以接受的。


4
由于几乎所有CPU架构上的除法指令都会产生余数和商,因此,实现3的速度是实现2的两倍,这表明您的编译器的优化效果不佳,在实现2中使用了两个除法指令。 4.7.1实现2实际上比实现3快4%,因为不需要额外的乘法。如果切换两个if子句,则编译器的性能可能会更好。实际上,这种return false情况更有可能,因此仅出于这个原因就值得切换(提速1%)。
2012年

1
O(ln(p)* sqrt(p))很遥远。O(sqrt(p))是素数的试验划分测试的复杂性,它们之间的间隔为O(ln(p))步长,但是中间的所有这些数字都是合成的,而不是素数。如果我读正确,平均复杂度为O(LN(LN(P))*的sqrt(P)/ LN(P))。
尼斯(Ness Ness)

2
@WillNess:<耸肩>我只是引用接受的答案。确切的复杂性是我回答的一个附带问题,它的存在是为了证明各种实施策略对性能的影响。
霍华德·

44

以防万一有人好奇:

使用反射器,我确定.Net使用静态类,其中包含〜72个素数的硬编码列表,范围最大为7199369,它扫描的最小素数至少是当前大小的两倍,并且扫描的大小大于它计算出的素数。通过所有奇数的尝试除法得到下一个素数,直至潜在数的平方根。此类是不可变的,并且是线程安全的(即,不会存储较大的质数以备将来使用)。


好答案。检查每个奇数不是确定素数的最有效方法,但大概绝大多数词典包含的键数不足350万。
柯克·布罗德赫斯特


3
我忽略了要提到的问题,尽管它似乎只尝试以奇数开头,因为它在寻找下一个质数时会递增2,因此它首先测试了除数。但是,还有一个奇怪的地方,那就是如果您要调用HashHelpers.GetPrime(7199370),它将遍历从7199370到2147483646的所有偶数而没有找到质数,然后只返回7199370。有趣的是,当然内部的,因此可能永远不会以这种方式调用它。
保罗·惠勒

3
h,我错了,有一个偷偷摸摸的按位OR或1,它将偶数输入值变成下一个更大的奇数。
保罗·惠勒2010年

1
但是,实际上不是我的问题的答案。
John Shedletsky 2010年

12

一个不错的技巧是使用部分筛子。例如,数字N = 2534536543556之后的下一个素数是什么?

相对于一系列小的素数检查N的模数。从而...

mod(2534536543556,[3 5 7 11 13 17 19 23 29 31 37])
ans =
     2     1     3     6     4     1     3     4    22    16    25

我们知道N之后的下一个素数必须是奇数,我们可以立即丢弃此小素数列表的所有奇数倍。这些模数使我们能够筛选出这些小质数的倍数。如果我们使用不超过200的小质数,我们可以使用此方案立即丢弃大多数大于N的潜在质数,除了一个小列表。

更明确地说,如果我们要寻找一个超出2534536543556的质数,则不能将其除以2,因此我们只需要考虑超出该值的奇数。上面的模数显示2534536543556与2 mod 3是一致的,因此2534536543556 + 1与0 mod 3是一致的,必须是2534536543556 + 7、2534536543556 + 13等。有效地,我们可以筛选出许多数字而无需任何操作测试它们的原始性,无需进行任何试验划分。

同样,事实是

mod(2534536543556,7) = 3

告诉我们2534536543556 + 4等于0 mod7。当然,这个数字是偶数,因此我们可以忽略它。但是2534536543556 + 11是一个可被7整除的奇数,例如2534536543556 + 25等。同样,我们可以将这些数字排除为清楚的复合数字(因为它们可以被7整除),因此不能以质数表示。

仅使用一小部分最多为37的质数,我们可以排除紧随起始点2534536543556之后的大多数数字,除了少数几个:

{2534536543573 , 2534536543579 , 2534536543597}

在这些数字中,它们是素数吗?

2534536543573 = 1430239 * 1772107
2534536543579 = 99833 * 25387763

我已尽力提供列表中前两个数字的素因式分解。看到它们是复合的,但主要因素很大。当然,这是有道理的,因为我们已经确保剩余的数不能有小的素数。实际上,我们的候选清单中的第三个(2534536543597)是除N之外的第一个素数。我描述的筛分方案将倾向于得出质数或由通常较大的质数组成的数。因此,在找到下一个素数之前,我们实际上只需要对几个素数进行显式检验。

类似的方案很快产生超过N = 1000000000000000000000000000000000的下一个素数,即1000000000000000000000000103。


您的确切含义是“我们知道N之后的下一个素数必须是奇数,我们可以立即丢弃此小素数列表中的所有奇数倍。这些模数使我们能够筛选出这些小素数的倍数。” ?
venkatKA 2012年

@zander-我添加了更多解释。

有道理!感谢ya
venkatKA 2012年

12

仅有几个关于本素间距离的实验。


这是可视化其他答案的补充。

我得到了从第100.000(= 1,299,709)到第200.000(= 2,750,159)的素数

一些数据:

Maximum interprime distance = 148

Mean interprime distance = 15  

互质距离频率图:

替代文字

质数距离与质数

替代文字

只是看到它是“随机的”。但是...


5

没有函数f(n)计算下一个素数。相反,必须测试一个数字的素性。

查找第n个质数时,也已经知道从第1个素数到第(n-1)个素数,这也是非常有用的,因为这些是唯一需要作为因子进行检验的数。

由于这些原因,如果预先计算出一组大质数,我不会感到惊讶。如果需要重复重新计算某些素数,对我来说真的没有意义。


您不需要知道从sqrt(p(n))到p(n-1)的素数。
Sparr 2010年

@Sparr同意,但是您将需要这些来计算p(m),其中p(m)> = p(n)。编写素数算法时,请将所有遇到的素数保留为“以后使用”。
柯克·布罗德赫斯特

有没有可证明没有这样的功能呢?还是缺乏想象力的证明?
约翰·谢列斯基

没有已知的功能,因此没有可用的功能,因此对于所有意图和目的都没有功能。如果有这样的功能,那么就不需要研究或研究素数,对吗?
柯克·布罗德赫斯特

3

正如其他人已经指出的那样,在给定当前素数的情况下,尚未找到找到下一个素数的方法。因此,由于必须检查已知素数和下一个素数之间的n / 2个数,因此大多数算法都将重点放在使用快速素数检查方法上。

Paul Wheeler所述,根据应用程序的不同,您还可以仅对查找表进行硬编码。


3

对于纯粹的新颖性,总有这种方法:

#!/usr/bin/perl
for $p ( 2 .. 200  ) {
      next if (1x$p) =~ /^(11+)\1+$/;
      for ($n=1x(1+$p); $n =~ /^(11+)\1+$/; $n.=1) { }
      printf "next prime after %d is %d\n", $p, length($n);
}

当然会产生

next prime after 2 is 3
next prime after 3 is 5
next prime after 5 is 7
next prime after 7 is 11
next prime after 11 is 13
next prime after 13 is 17
next prime after 17 is 19
next prime after 19 is 23
next prime after 23 is 29
next prime after 29 is 31
next prime after 31 is 37
next prime after 37 is 41
next prime after 41 is 43
next prime after 43 is 47
next prime after 47 is 53
next prime after 53 is 59
next prime after 59 is 61
next prime after 61 is 67
next prime after 67 is 71
next prime after 71 is 73
next prime after 73 is 79
next prime after 79 is 83
next prime after 83 is 89
next prime after 89 is 97
next prime after 97 is 101
next prime after 101 is 103
next prime after 103 is 107
next prime after 107 is 109
next prime after 109 is 113
next prime after 113 is 127
next prime after 127 is 131
next prime after 131 is 137
next prime after 137 is 139
next prime after 139 is 149
next prime after 149 is 151
next prime after 151 is 157
next prime after 157 is 163
next prime after 163 is 167
next prime after 167 is 173
next prime after 173 is 179
next prime after 179 is 181
next prime after 181 is 191
next prime after 191 is 193
next prime after 193 is 197
next prime after 197 is 199
next prime after 199 is 211

除了所有娱乐和游戏,众所周知,最佳哈希表大小严格证明是该格式的素数4N−1。因此,仅找到下一个素数是不够的。您也必须做其他检查。


2
谁知道您可以使用正则表达式搜索素数:/
Nikita Rybak 2010年

1
有趣的是,保罗·惠勒(Paul Wheeler)的回答表明MS使用至少一个4N + 1素数。
史蒂夫·杰索普

0

据我所记得,它在当前大小的两倍旁边使用质数。它不会计算素数-在该表中预载了一些最大数值的数字(不完全是大约10,000,000左右的数字)。达到该数字时,它会使用一些朴素的算法来获取下一个数字(例如curNum = curNum + 1),并通过以下方法使用某些方法对其进行验证:http : //en.wikipedia.org/wiki/Prime_number#Verifying_primality


仅存在一对素数,使得curNum和curNum + 1都是素数。更加努力。
tchrist

尝试next_prime = prime + 2。你可能是对的,没有人能证明一旦你足够高,你就永远错了。所以去吧。:)
tchrist 2010年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.