Reposting it in a clear, understandable way without any complicated MathJax that doesn't appear properly:
I explored some computer science/number theory challenges sites for fun, and they presented the following problem, exactly as follows:
Let P(n) = sum{1<=k<=n} φ(k)
Find P(10^16)
I searched for quite a while about this and tried different approaches:
Using the formula for
φ(n)= n * product{1<=i<=k} (Pi-1)/Pi
, I tried to calculate theφ(n)
in range, but this becomes very inefficient for largen
. I could get as far as10^7
with this approach. Beyond this it just gets too slow.I tried a different one, more direct. Wikipedia and Wolfram Alpha suggest similiar formulas for directly calculating
P(n)
:
P(n) = sum {1<=k<=n} φ(k)= 0.5⋅(1+∑{1<=k<=n} μ(k)⋅⌊n/k⌋^2)
This formula seemed a lot more promising. I tried it and managed to get alot further than 10^7
but still far from the target. With pre-calculating a sieve of the Moebius function, I could get to a bit less than 10^9
. My memory was insufficient, and couldn't compute anymore values in a sieve. And even if I could, it still takes a long time and is very far from 10^16
.
Here is part of the code that I used for my 2nd approach written in Java:
public static BigInteger PhiSummatoryFunction (long limit)
{
BigInteger sum = BigInteger.ZERO;
int [] m = MoebiusSieve(limit);
for (int i=1;i<m.length;i++)
sum=sum.add(BigInteger.valueOf((long) (m[i]*Math.floor(limit/i)*Math.floor(limit/i))));
return sum.add(BigInteger.ONE).divide(BigInteger.ONE.add(BigInteger.ONE));
}
Where MoebiusSieve is a function that computes the Moebius function values up to a certain limit in a sieve, using an eratosthenes-like method.
- After understanding and implementing a new recursive method for this computation that I found on the internet:
P(n)=n(n+1)/2−∑{2<=i<=√n} P(⌊n/i⌋)−∑{1<=j<=√n} P(j)⋅(⌊n/j⌋−⌊n/(j+1)⌋)
I can compute values up to P(10^11)
, and with maximum memory allocation, pre-computing as many φ(n) as possible and consequently all P(n)
that I can for memoization, I can compute P(10^12)
in just over 20 minutes. A major improvement but still a little far from P(10^16)
. It's ok if the computation takes a bit longer, but I fear P(10^16)
would take exponentially longer time, judging by the "jump" in computation time between P(10^11)
and P(10^12)
. My memory allows me to "save" up to 350,000,000 φ(n) values
OR up to 700,000,000 μ(k) values
. Perhaps there is a way to perform the summation using μ(k) values rather than φ(n)?.
All my computations suggest and show that my recursion is the prominent time consumer. This is obvious, but I am sure it takes longer than it should. I am posting below the recursion code, with some documentation. It seems to me this is the right way to do this computation, but my implementation is not optimal.
public static BigInteger phiR (long limit, long [] s) // limit is 10^t, s is the sieve of precomputed values of `P(n)`. Can store maximum 350,000,000 values
{
if (limit<s.length)
return BigInteger.valueOf(s[(int) limit]);
BigInteger sum = BigInteger.valueOf(limit).multiply(BigInteger.valueOf(limit).add(BigInteger.ONE)).divide(BigInteger.valueOf(2)); // this corresponds to the n'th triangular number
BigInteger midsum1=BigInteger.ZERO; // the first sum
BigInteger midsum2=BigInteger.ZERO; // the second sum
long m = 2;
while (limit/m != limit/(m+1) && m*m<=limit) // computing the first sum, first for changing floor(limit/m) values
{
midsum1=midsum1.add(phiR((long) Math.floor(limit/m),s));
m++;
}
for (long k = m;k*k<=limit;k++) // once the floors become constant for some values,-->
{ // can check how many times the value appears, and multiply accordingly,-->
BigInteger midPhi = phiR((long) Math.floor(limit/k),s); // rather than compute the Phi every time
long q = 1;
while (limit/k==limit/(k+1)&&k*k<=limit)
{
q++;
k++;
}
k--;
midPhi=midPhi.multiply(BigInteger.valueOf(q));
midsum1=midsum1.add(midPhi);
}
for (long d=1;d*d<=limit;d++) // computing the second sum
if ((double)d!=Math.floor(limit/d))
midsum2=midsum2.add(BigInteger.valueOf((long) (Math.floor(limit/d)-Math.floor(limit/(d+1)))).multiply(phiR(d,s)));
sum=sum.subtract(midsum1).subtract(midsum2);
return sum;
}
I was suggested to use dictinaries, in addition to the array, for big values of n
, but I don't know anything about it. Can another improvement be made to make the time frame a day or so?