10

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

下一个素数非常简单,半素数就不那么简单了。需要明确的是,只需要半素数,尽管同时获取因子会很方便。

我已经想到了一些方法,但我相信还有更好的方法。

算术运算假定为 O(1)。我使用了埃拉托色尼筛,这是 O(n log log n),我知道阿特金筛,但我喜欢我的半优化 Eratosthenes。

1.从n开始数

从 n 开始数,当你找到一个半素数时停止。

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

半素数分布似乎远高于素数分布,因此通过良好的半素数测试,这实际上可能比 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)>>1)<<1)|1。虽然 pq 小于迄今为止的最小值并且 q 不是素数,但将 2 添加到 q。如果 pq 仍然小于最小值,则将其记录为新的最小值。


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

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

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

4

5 回答 5

1

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

  1. is_semiprime(n)

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

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

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

  1. next_semiprime(n)

在 1e16,到下一个半素数的平均距离小于 10,很少超过 100。和以前一样,如果您想进行预计算,使用内存,并且可以忽略或摊销设置时间,这可以很快得到解决。但是一旦过去的小输入再次变得非常麻烦。

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

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

  1. 半素数(低,高)

有些答案似乎在思考这个问题。特别是当 low <= 4 时,筛子是正确的答案。有用于 totients 和 moebius 范围的快速筛分方法,我希望您可以将一种方法调整为全因子计数。

注意:编写良好的 SoE 比 SoA 更快,所以不要被推荐阿特金筛子的人分心,因为他们可能刚刚阅读了 Wikipedia 页面的前几段。当然,筛子、素性测试和预测试的实施细节将对结论产生影响。缓存数据的预期输入大小、模式和容忍度也是如此。

于 2017-03-10T22:33:55.937 回答
0

Meir-Simchah Panzer 在https://oeis.org/A001358上建议“这个序列的等效定义是...... [the] 最小的合数,它不被任何更小的合数除。”

下面是根据这个想法计算 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
于 2017-02-27T05:33:42.910 回答
0

您可以预先计算所有半素数,然后使用二进制搜索。百万以下有 80k 个素数,因此 10^12 以下有 30 亿个半素数。可能不会花太多时间。

于 2017-02-26T19:33:59.720 回答
0

这是基于我上面评论的一些代码:我们将运行 Eratosthenes 筛,但在我们执行此操作时,会存储一些额外的数据,而不仅仅是“素数或非素数”。它在 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的形式值和,或者是值。所以这有点像一个最多两个整数长的 s 列表(或者一个说明它是三个整数或更长的注释)。我们可以将因子添加到这样的列表中:TwoFactor n n'nn'ManyFactorInteger

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 的假设,对于任何,在和n之间都有一个素数;这意味着在和之间有一个半素数(即假设给我们的素数的两倍)。更重要的是,任何这样的半素数都不能有一个大于的因子(因为那时另一个因子必须小于!)。因此,我们将根据 至的因子筛选 至 的数字,然后检查 和 之间的数字是否为半素数。因为后一种检查是 O(1),所以我们属于您提出的第一种情况。所以:n/2nn2nn22nnn2n

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

2创建一个索引在和之间的数组,在每个位置2n初始化。Prime

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

对于和...p之间的每个潜在素数2n

    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+1搜索2n。每次调用都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函数只是读取一个数组元素并写回修改后的版本。arr[ix] = f(arr[ix]);在 C 中思考。

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

由于 Haskell 对数组的处理方式变幻莫测,因此newSTArray需要它:所有数组操作在您使用的数组类型上都是多态的,这既方便又烦人。这告诉编译器我们想要这个程序使用哪种类型的数组。

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

您可以在这里尝试一下,其中包括一个简单main的打印前 100 个半素数的方法。(尽管如果这是目标,后一项任务可以以更有效的方式完成!)

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

于 2017-02-26T21:58:24.660 回答
0

跟进对@DanielWagner的建议提出的评论(现已删除) ,这里是一个未优化的半素筛,每个条目使用两位以保持因子计数。

筛分条目包含发现的因子数量,限制为 3。标记通过对相关筛分条目进行饱和增量。

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

假设筛子不包含偶数的条目(像往常一样),我们将半素数 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);
  }
}

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

于 2017-02-26T22:39:55.237 回答