更好的算法 - 下一个半刚性

时间:2017-02-26 19:11:07

标签: algorithm optimization primes sieve-of-eratosthenes primality-test

  

给定n,找到m使得m是大于n的最小半素。

下一个素数非常简单,半素数不那么明显。需要说明的是,只需要半素数即可,但同时获取因子也很方便。

我想过几种方法,但我确信有更好的方法。

假设算术运算为O(1)。我使用了Eratosthenes的Sieve,它是O(n log log n),我知道Atkin的Sieve,但我喜欢我的半优化的Eratosthenes。

1。从n

算起

从n开始向上计数,当你找到半数时停止。

这看起来真的很愚蠢,但如果有一个O(log n)半素测试或O(1)测试给出下面的素数,这可能比其他2更快。

Semiprime分布似乎远高于素数分布,因此通过良好的半素测试,这实际上可能优于O(n)。

2。倒数计数

定义prev(x)和next(x)并分别给出上一个和下一个素数,如果素数存储在树中或者使用列表二进制搜索,则可以是O(log n)。

首先筛选。

从p = prev(sqrt(n))开始,q = next(n / p)。当pq <= n时,转到下一个q。如果pq小于目前为止的最小值,则将其记录为新的最小值。继续前一个p,直到用完p进行测试。

这可以保证找到正确答案,但速度相当慢。仍然是O(n log n),所以也许可以接受。

3。赞美数

像往常一样从筛子开始。为O(1)素性测试创建筛子的哈希集视图。

从p = 2开始。通过素数迭代到sqrt(n)。对于每个p,得到q =(((n / p + 1)/ 2)* 2)+1 =(((n / p + 1)>&gt; 1)&lt;&lt; 1)| 1。虽然到目前为止pq小于最小值且q不是素数,但是将q加2。如果pq仍小于最小值,则将其记录为新的最小值。

我在Java中实现了#1和#3,两者都使用了相同的Eratosthenes Sieve实现。大部分的运行时间都花在筛选上,所以如果要进行优化,那就是在筛子中。经过一些优化后,计数(#1)击败了数字(#3),在最后一次和最大的测试(11位十进制数n)中快两倍。

但仍然有希望,因为筛子需要延伸多远取决于最大数量的主要测试。如果存在具有较低质数测试界限的半素测试,则计数方法可能会更快。

当然有更好的算法?或者至少是一种更好的方法来实现这个?

5 个答案:

答案 0 :(得分:0)

你可以预先计算所有的半素数,然后使用二元搜索。有百万以下的80k素数,所以在10 ^ 12下的30亿次半数可能不会花费太多时间。

答案 1 :(得分:0)

以下是基于我上面评论的一些代码:我们将运行一个Eratosthenes的Sieve,但是存储一些额外的数据,而不仅仅是&#34; prime或not&#34;在我们这样做的同时。它在Haskell中,我认为它不是最常用的语言,因此我将对每个位的内容进行内联评论。首先是一些图书馆进口:

import Control.Monad
import Control.Monad.ST
import Data.Array.ST
import Data.Maybe

我们将定义一种新类型Primality,我们将使用该类型存储每个号码最多两个素数因素。

data Primality
    = Prime
    | OneFactor Integer
    | TwoFactor Integer Integer
    | ManyFactor
    deriving (Eq, Ord, Read, Show)

这表示类型Primality有四种值:值为Prime,某些无界整数OneFactor n的格式为n,两个无界整数TwoFactor n n'n的格式n'的值,或值ManyFactor。所以这有点像Integer的列表,其中最多两个整数长(或者说一个说明它是三个整数长或长)。我们可以将因子添加到这样的列表中:

addFactor :: Integer -> Primality -> Primality
addFactor f Prime = OneFactor f
addFactor f (OneFactor f') = TwoFactor f f'
addFactor _ _ = ManyFactor

考虑到数字的素数因子列表,很容易检查它是否是半素数:它必须至多有两个较小的素数因子,其乘积就是数字本身。

isSemiprime :: Integer -> Primality -> Bool
isSemiprime n (OneFactor f   ) = f * f  == n
isSemiprime n (TwoFactor f f') = f * f' == n
isSemiprime _ _ = False

现在我们来写Sieve。根据Bertrand的假设,对于任何nn/2n之间存在一个素数;这意味着在n2n之间存在一个半刚性(即,假设给我们的素数是两倍)。更重要的是,任何这样的半素都不能有大于n的因子(从那时起,另一个因素必须小于2!)。因此,我们会根据2n的因子筛选最多n的数字,然后检查n2n之间的数字,以查找半数。因为后面的检查是O(1),所以我们在你提出的第一种情况下。所以:

nextSemiprime :: Integer -> Integer
nextSemiprime n = runST $ do

制作一个索引在22n之间的数组,在每个位置初始化为Prime

    arr <- newSTArray (2,2*n) Prime

对于p2之间的每个潜在素数n ...

    forM_ [2..n] $ \p -> do

......我们目前认为是Prime ...

        primality <- readArray arr p
        when (primality == Prime) $

...为p的每个倍数添加p到因子列表。

            forM_ [2*p,3*p..2*n] $ \i ->
                modifyArray arr i (addFactor p)

现在对n+12n之间的剩余数字进行线性搜索以获得半素数。每次调用isSemiprime都需要一次乘法,因此他们需要O(1)。从技术上讲,搜索可能会失败; fromJust <$>注释告诉编译器我们保证它不会失败(因为我们已经完成了一些太复杂而无法传输到编译器的离线数学证明)。

    fromJust <$> findM (\i -> isSemiprime i <$> readArray arr i) [n+1..2*n]

那是nextSemiprime的整个身体。它使用了一些辅助函数,这些函数确实应该在某个标准库中。首先是线性搜索算法;它只是在列表中查找满足谓词的元素。

findM :: Monad m => (a -> m Bool) -> [a] -> m (Maybe a)
findM f [] = return Nothing
findM f (x:xs) = do
    done <- f x
    if done then return (Just x) else findM f xs

modifyArray函数只读取一个数组元素并写回修改后的版本。在C中考虑arr[ix] = f(arr[ix]);

modifyArray :: (MArray a e m, Ix i) => a i e -> i -> (e -> e) -> m ()
modifyArray arr ix f = readArray arr ix >>= writeArray arr ix . f

并且需要newSTArray,因为Haskell对数组的处理变幻莫测:所有数组操作都是你所使用的数组的多态,这同时也很方便和烦人。这告诉编译器我们想要这个程序使用哪种数组。

newSTArray :: Ix i => (i,i) -> e -> ST s (STArray s i e)
newSTArray = newArray

您可以试用here,其中包含一个简单的main,用于打印前100个半成品。 (虽然如果这是目标,后面的任务可以用更多,更有效的方式完成!)

虽然当前算法只返回下一个半素值,但很容易修改它以返回下一个半素值的分解:只返回相关的Primality值而不是Integer本身

答案 2 :(得分:0)

对来自@DanielWagner的建议发表评论(现已删除)后,这里是一个非优化的半素筛,每个条目使用两位,以保持因子数。

筛选条目包含发现的因子数量,限制为3.标记过程会使相关筛分条目的饱和度增加。

因为我们也关心两个相同的因素,我们也筛选素数的平方。在筛子期间可以识别质数的幂,因为它们的因子计数将是1(质数计数为0;半数2和其他整数3)。当我们标记素数的平方(这将是遇到的素数的第一个幂)时,我们可以对每个条目进行饱和加2,但作为微优化,代码只是将计数直接设置为3。 / p>

假设筛子不包含偶数的条目(通常情况下),我们特殊情况下半素4和所有半因子的因子为2且奇数素数。

以下代码在(伪)C ++中,仅显示如何进行筛分操作。一些细节,包括saturating_increment的定义和其他筛选访问函数,已被省略,因为它们很明显,只会分散注意力。

/* Call `handler` with every semiprime less than `n`, in order */ 
void semi_sieve(uint32_t n, void(*handler)(uint32_t semi)) {
  Sieve sieve(n);
  if (n > 4) handler(4); /* Special case */
  for (uint32_p p = 3; p < n; p += 2) {
    switch (sieve.get(p)) {
      case 0: /* A prime */
        for (uint32_t j = p + p + p; j < n; j += p + p)
          sieve.saturating_increment(j);
        break;
      case 1: /* The square of a prime */
        handler(p);
        for (uint32_t j = p + p + p; j < n; j += p + p)
          sieve.set(j, 3);
        break;
      case 2: /* A semiprime */
        handler(p);
        break;
      case 3: /* Composite non-semiprime */
        break;
      default: /* Logic error */
    }
    /* If the next number might be twice an odd prime, check the sieve */
    if (p + 1 < n && p % 4 == 1 && 0 == sieve.get((p + 1)/2))
      handler(p + 1);
  }
}

注意:我知道上面的扫描是使用整个范围的素数而不是直到平方根。这必须付出一些代价,但我认为这只是常数的变化。可以提前终止扫描,获得一些常数。

答案 3 :(得分:0)

Meir-Simchah Panzer在https://oeis.org/A001358上建议“这个序列的等价定义是...... [最小的复合数字,不会被任何较小的复合数除。”

以下是依据这个想法计算100后的下一个半素的例子:

Mark numbers greater than n that are not semiprime and stop when
you've skipped one that's not prime.

 2 * 51 = 102, marked
 3 * 34 = 102, marked
 5 * 21 = 105, marked
 7 * 15 = 105, marked
11 * 10 = 110, marked
13 *  8 = 104, marked
17 *  6 = 102, marked

101,103,107,109 are prime and we skipped 106 and 108
The only two primes that could cover those in our
next rounds are 2 and 3:

2 * 52 = 104
3 * 35 = 105

Third round:
2 * 54 = 108
3 * 36 = 108

We skipped 106

答案 4 :(得分:0)

人们正在回答略有不同的问题,所以让我把它分成几个部分。

  1. is_semiprime(n)的
  2. 给定值n,它是半素数。对于微小输入,我们当然可以预先计算并返回O(1)或搜索中的答案。在某些时候,我们会被存储要求所淹没。据我所知,这个问题没有非常有效的解决方案。它类似于素数或无平方测试,因为我们可以通过简单的可分性测试快速清除大多数随机输入。假设我们有一个快速素性测试,包括一些预测试,大部分工作只是寻找一个小因子,然后返回余数是否为素数。对于没有小因子的数字,我们可以做一些因子分解(例如Brent / Pollard Rho)或试验分割到n ^(1/3)。

    在我的Macbook上,1e8到1e7 + 1e7范围内每个数字大约需要0.4微秒,1e16到1e16 + 1e7范围内每个数字小于2微秒。

    对于大型半素数或近半数,我不确定有没有比找到单个因子更好的解决方案。我们要求试验除以N ^(1/3),但有更有效的标准分解算法。一些实现包括Charles Greathousemine和许多RosettaCode

    1. next_semiprime(n)的
    2. 在1e16,到下一个半素数的平均距离小于10且很少超过100.如前所述,如果你想进行预计算,使用记忆,并且可以忽略或分摊设置时间,这可以回答很快。但是,一旦过去的小投入再次变得非常麻烦。

      我认为只要一个简单的while (1) { n++; if (is_semiprime(n)) return n; }假设一个好的is_semiprime例程,你就不会有显着的改进。对我来说,完全筛选出来要慢得多,但你的里程可能会有所不同。一旦你跨越~25位输入,它真的不会执行。您可以通过使用具有递增因子计数的主要功率的部分筛来略微优化,这意味着我们仅需要对不明显为半素数的结果运行完整测试。我没有多少时间节省,这是有道理的,因为我们只删除了一些本机模数。如果我们查看1000位数输入,那么我认为部分筛选很有意义。

      在我的Macbook上,next_semiprime使用从1e8开始连续调用1e6次的简单is_semiprime方法,每次调用大约需要2微秒,从1e16开始每次调用需要17微秒。

      1. 半成品(低,高)
      2. 有些答案似乎在考虑这个问题。特别是当低<≤4时,筛子是正确的答案。对于totients和moebius范围有快速筛选方法,我希望你可以调整一个到全因子数。

        注意:编写良好的SoE比SoA快,所以不要被推荐Atkin Sieve的人分心,因为他们可能只是阅读了维基百科页面的前几段。当然,筛子的实施细节,素性测试和预测试将对结论产生影响。与缓存数据的预期输入大小,模式和容忍度一样。