如何有效筛选选定的素数范围?

时间:2012-12-22 15:04:44

标签: ruby primes mathematical-optimization

我一直在研究Project Euler和Sphere Online Judge问题。在这个特殊问题中,我必须找到两个给定数字内的所有素数。我有一个看起来很有前途的功能(基于Sieve of Eratosthenes),除了它太慢了。有人能发现什么会减慢我的功能,并提示我如何解决它?此外,我们将非常感谢有关如何进行优化的一些评论(或指向此类评论/书籍/文章等的链接)。

代码:

def ranged_sieve(l, b)
  primes = (l..b).to_a
  primes[0]=nil if primes[0] < 2
  (2..Math.sqrt(b).to_i).each do |counter|
    step_from = l / counter
    step_from = step_from * counter
    l > 3 ? j = step_from : j = counter + counter
    (j..b).step(counter) do |stepped|
      index = primes.index(stepped)
      primes[index] = nil if index
    end
  end
  primes.compact
end

2 个答案:

答案 0 :(得分:3)

SPOJ(Sphere Online Judges)的PRIME1 problem的设计是为了让你不能简单地筛选到上限,因为在这种情况下你会被超时击中。

一种可能的方法是提高速度;通过在标准筛上添加一些铃铛和口哨,可以使其运行得足够快以保持远低于超时限制。速度优化包括:

  • 仅代表筛子中的奇数整数(节省50%空间)
  • 在符合L1缓存(32 KB)的小型缓存友好段中进行筛选
  • 通过小素数表示(即在筛网段上喷射预先计算的图案)
  • 记住跨段的每个素数的最后(或下一个)工作偏移量,而不是使用慢速分割重新计算它们

将所有这些放在一起可以将整个2 ^ 32范围的时间从20秒缩短到2秒,远低于SPOI超时。 My pastebin有三个简单的C ++演示程序,您可以在其中检查每个优化操作并查看其效果。

更简单的方法是仅进行必要的工作:筛选目标范围的最后一个数的平方根以获得所有潜在的素因子,然后仅筛选目标范围本身。这样你就可以在不到二十几行代码和几毫秒运行时解决SPOJ问题。我刚刚完成了一个demo .cpp for this type of segmented sieving(困难的部分不是筛子,而是测试框架,用于舒适的测试,并且由于几乎没有任何参考数据,因此验证了正确的操作,最多2 ^ 64-1)。

筛子本身看起来像这样;筛子是一个仅有赔率的打包位图,并且筛子范围以位为单位指定以获得稳健性(它在.cpp中都有解释),因此您将为offset传递(range_start / 2):

unsigned char odd_composites32[UINT32_MAX / (2 * CHAR_BIT) + 1];   // the small factor sieve
uintxx_t sieved_bits = 0;                                          // how far it's been initialised

void extend_factor_sieve_to_cover (uintxx_t max_factor_bit);       // bit, not number!

void sieve32 (unsigned char *target_segment, uint64_t offset, uintxx_t bit_count)
{
   assert( bit_count > 0 && bit_count <= UINT32_MAX / 2 + 1 );

   uintxx_t max_bit = bit_count - 1;
   uint64_t max_num = 2 * (offset + max_bit) + 1;
   uintxx_t max_factor_bit = (max_factor32(max_num) - 1) / 2;

   if (target_segment != odd_composites32)
   {
      extend_factor_sieve_to_cover(max_factor_bit);
   }

   std::memset(target_segment, 0, std::size_t((max_bit + CHAR_BIT) / CHAR_BIT));

   for (uintxx_t i = 3u >> 1; i <= max_factor_bit; ++i)
   {
      if (bit(odd_composites32, i))  continue;

      uintxx_t n = (i << 1) + 1;   // the actual prime represented by bit i (< 2^32)

      uintxx_t stride = n;         // == (n * 2) / 2
      uint64_t start = (uint64_t(n) * n) >> 1;
      uintxx_t k;

      if (start >= offset)
      {
         k = uintxx_t(start - offset);
      }
      else // start < offset
      {
         uintxx_t before_the_segment = (offset - start) % stride;

         k = before_the_segment == 0 ? 0 : stride - before_the_segment;
      }

      while (k <= max_bit)
      {
         set_bit(target_segment, k);

         // k can wrap since strides go up to almost 2^32
         if ((k += stride) < stride)
         {
            break;
         }
      }
   }
}

对于SPOJ问题 - 小于2 ^ 32的数字 - 无符号整数足以用于所有变量(即uint32_t而不是uintxx_t和uint64_t),并且可以进一步简化某些事情。另外,对于这些小范围,您可以使用sqrt()代替max_factor32()

在演示代码中,extend_factor_sieve_to_cover()在小的缓存友好步骤中完成了sieve32(odd_composites32, 0, max_factor_bit + 1)的道德等同。对于SPOJ问题,您可以简单地使用单个sieve32()调用,因为只有6541个小奇数素数因子小于2 ^ 32,您可以立即筛选。

因此解决这个SPOJ问题的诀窍是使用分段筛分,将总运行时间缩短到几毫秒。

答案 1 :(得分:0)

我没有充分了解,但有一个因素是,您正在使用primes替换nil中的某个值,然后使用compact替换它以删除它们。这是一种浪费。只需直接使用delete_at进行操作即可快速完成两次:

def ranged_sieve2(l, b)
  primes = (l..b).to_a
  primes.delete_at(0) if primes[0] < 2
  (2..Math.sqrt(b).to_i).each do |counter|
    step_from = l / counter
    step_from = step_from * counter
    l > 3 ? j = step_from : j = counter + counter
    (j..b).step(counter) do |stepped|
      index = primes.index(stepped)
      primes.delete_at(index) if index
    end
  end
  primes
end

ranged_sieve(1, 100) # => Took approx 8e-4 seconds on my computer
ranged_sieve2(1, 100) # => Took approx 3e-4 seconds on my computer

要改进的另一点是,使用散列比数组快得多,因为相关的大小变大了。用哈希替换你的数组实现,你可以得到这个:

def ranged_sieve3(l, b)
  primes = (l..b).inject({}){|h, i| h[i] = true; h}
  primes.delete(0)
  primes.delete(1)
  (2..Math.sqrt(b).to_i).each do |counter|
    step_from = l / counter
    step_from = step_from * counter
    l > 3 ? j = step_from : j = counter + counter
    (j..b).step(counter) do |stepped|
      primes.delete(stepped)
    end
  end
  primes.keys
end

当您使用此range_sieve3(1, 100)时,由于开销,它比range_sieve2(1, 100)慢。但随着你的数字变大,优势变得显着。例如,我在计算机上得到了这个结果:

ranged_sieve(1, 1000) # => Took 1e-01 secs
ranged_sieve2(1, 1000) # => Took 3e-02 secs
ranged_sieve3(1, 1000) # => Took 8e-04 secs