我一直在研究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
答案 0 :(得分:3)
SPOJ(Sphere Online Judges)的PRIME1 problem的设计是为了让你不能简单地筛选到上限,因为在这种情况下你会被超时击中。
一种可能的方法是提高速度;通过在标准筛上添加一些铃铛和口哨,可以使其运行得足够快以保持远低于超时限制。速度优化包括:
将所有这些放在一起可以将整个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