Multiples of 2 cannot be prime and primes greater than 2 are odd. This means that the sum to be found must be the sum of an odd number of primes if 2 is not in the window. This lets you skip half the work.
You can update a sliding sum by subtracting the outgoing number and adding the incoming one. Perhaps this could be the basis of a window that moves forward as it expands and contracts, much like a caterpillar moves... The idea is to avoid redoing lots of sums without having to store lots of intermediary results. The basic sliding sum should already save quite a bit of work, though.
This can give you a quick exploratory run with sequences (sums) of maximum possible length, if you expand two sums from 2 and 5 respectively - skipping every other prime - and start sliding once the sums exceed the maximum prime. The disadvantage is that you do not gain information for bounding future searches, as in the bottom-up case where you never have to consider sequences shorter than the longest you've found so far. Perhaps it is better to caterpillar right from the start, to quickly collect growing lower bounds on sequence length.
Growing pair of snakes: slide two sums forward, offset by one prime, that are one double step longer than the longest sequence found so far. Expand as you find longer and longer sequences. That should give you some decent results in one stereo slither through the range, which can help bound future searches. (hindsight: one snake, single steps; it's only the growing that can be double-stepped)
UPDATE
I did a bit of snake charming and it didn't pan out - snakes grow very slowly and they have to move around a lot in order to do so. By and large there is little that can be done in the way of improving/refining bounds for the search, apart from using the length of the best sequence found so far (plus two) as a lower bound for future searches.
A lot of re-computation can be avoided by computing cumulative sums for the primes and storing them in a table; that way the sum from prime i
to prime k
is available as p_sums[k] - p_sums[i - 1]
for i
greater than zero.
Also, searches go faster if all possible sequence lengths are tried for a given starting point before moving to the next starting point, rather than fixing the sequence length for a given round and then trying all starting points. However, I haven't studied this point thoroughly; the success of this strategy may depend on the fact that a lot of winning sequences happen to begin at very low primes for the small range I tested (232).
'Brute force with superior firepower' is several orders of magnitude faster than the plain brute-force algorithm:
*** g++ 4.8.1 64 ***
v0: 10 0.000 ms 5 = {2} @ 2
v1: 10 0.000 ms 5 = {2} @ 2
v0: 100 0.001 ms 41 = {6} @ 2
v1: 100 0.000 ms 41 = {6} @ 2
v0: 1000 0.010 ms 953 = {21} @ 7
v1: 1000 0.001 ms 953 = {21} @ 7
v0: 10000 0.122 ms 9521 = {65} @ 3
v1: 10000 0.002 ms 9521 = {65} @ 3
v0: 100000 1.505 ms 92951 = {183} @ 3
v1: 100000 0.013 ms 92951 = {183} @ 3
v0: 1000000 19.793 ms 997651 = {543} @ 7
v1: 1000000 0.104 ms 997651 = {543} @ 7
v0: 10000000 254.493 ms 9951191 = {1587} @ 5
v1: 10000000 0.782 ms 9951191 = {1587} @ 5
v0: 100000000 4485.791 ms 99819619 = {4685} @ 7
v1: 100000000 6.226 ms 99819619 = {4685} @ 7
v0: 1000000000 68065.223 ms 999715711 = {13935} @ 11
v1: 1000000000 52.905 ms 999715711 = {13935} @ 11
v0: 10000000000 1359343.949 ms 9999419621 = {41708} @ 2
v1: 10000000000 485.861 ms 9999419621 = {41708} @ 2
The last round used more than 7 GB of core for the two tables, which means this is pretty much the end of the line for this simple algorithm even though the actual search took only half a second. The time for sieving the primes - about one second per 2^31 numbers for my simple code - is not included.
I'm showing the routine as C++ since my Python-fu is not sufficient for doing either python or the algorithm justice. At least that way you have an accurate description for the algorithm, and can ponder about devious ways of having Python do all the hard work:
typedef uint32_t num_t;
typedef uint64_t sum_t;
std::vector<num_t> primes;
std::vector<sum_t> p_sums; // sum(primes[0] .. primes[i]) for i < primes.size()
void prepare_prime_tables_for_limit (num_t n);
bool is_prime (num_t n); // uses std::lower_bound() on primes[], i.e. binary search
num_t brute_force_v1 (unsigned n = 1000000000)
{
assert( n >= 10 );
prepare_prime_tables_for_limit(n);
num_t prime_count = num_t(primes.size());
num_t max_prime = primes.back();
num_t best_len = 1;
num_t best_idx = 0;
sum_t best_sum = 0;
// separate pass for the prime that is so odd because it's even
for (num_t i = 1; ; i += 2)
{
sum_t sum = p_sums[i];
if (sum <= max_prime)
{
if (is_prime(num_t(sum)))
{
best_len = i + 1;
best_sum = sum;
}
}
else break;
}
--best_len; // adjust for the evenness of 2
for (num_t i = 1; i + best_len + 1 < prime_count; ++i)
{
for (num_t k = i + best_len + 1; ; k += 2)
{
sum_t sum = p_sums[k] - p_sums[i - 1];
if (sum <= max_prime)
{
if (is_prime(num_t(sum)))
{
best_len = k - i + 1;
best_idx = i;
best_sum = sum;
}
}
else break;
}
}
best_len += best_idx == 0; // restore the stolen bit if 2 is still best
// ... print stuff ...
return num_t(best_sum);
}
I left index math etc. unoptimised in order to make the code easier to understand. For the same reasons I didn't harden it by adding the complications needed for handling boundary cases (n < 8 etc.). My intent was to make the basic algorithm as clear as possible.
Sums need a bigger type than indexes and numbers, hence the occasional cast to num_t
when it is safe (and necessary) to do so.