Tricky Double
Other languages: Español 한국어 Português 中文
This text is a translation of the original article by my friend, Maksim Pelevin. If you are comfortable with reading Russian, you might find more interesting articles in his blog.
You already know this:
The double
data type is not very precise
Let’s reinforce this understanding by solving a problem on CodeWars.
Problem statement
The task as described on CodeWars is as follows:
Your task is to construct a building, which will be a pile of n cubes. The cube at the bottom will have a volume of n^3, the cube above will have volume of (n-1)^3 and so on until the top which will have a volume of 1^3.
You are given the total volume m of the building. Being given m, can you find the number n of cubes you will have to build?
The parameter of the function findNb
will be an integer m
and you have to return the integer n
such as
n^3 + (n-1)^3 + … + 1^3 = m if such a n exists or -1
if there is no such n.
Naive solution
The most straightforward solution using loops would be something like:
public static long findNb(long m) {
long n = 0, totalVolume = 0;
while (totalVolume < m) {
totalVolume += ++n * n * n;
}
return totalVolume == m ? n : -1;
}
Mathematical solution
Back in university, I learned about mathematical series and the methods to calculate their sums, so here is my take on a mathematical solution.
I no longer recall the formulas, but thankfully, Wolfram Alpha is here to help. To get the formula, let’s run the following query: n^3, n = 1 to k.
The results tell us that:
Let’s substitute m
for the left side of the equation,
then apply the square root property
and multiply both sides of the equation by two.
After simplifying, we get:
Write the code
Here is a solution for the equation above, written in Java:
public static long findNb(long m) {
double res = Math.sqrt(m);
double d = 1 + 8 * res;
double n = (Math.sqrt(d) - 1) / 2;
return n - Math.floor(n) == 0 ? (long) n : -1;
}
Results
It turns out that this code returns an incorrect result for a subset of test data.
This inconcistency is visible, for example, when
you pass 1646842954019151682L
as the input.
While calculating a square root in Java, you may get a result that isn’t wholly accurate. Let’s take a look:
System.out.println(String.format("%.30f", Math.sqrt(1646842954019151682L)));
// output 1283293791.000000000000000000000000000000
// correct 1283293791.00000000038962239473657673134031176401122912...
Obviously, the output and the correct answer are different. Fin!
Conclusion
The most technically correct and potentially fastest solution is not always the best one to choose. This may be no surprise to a skilled developer, but those new to programming might puzzle over the cause of the error for quite a while.
If you’re wondering how to fix the problem with the mathematical solution,
one approach is to use BigDecimal
. This code passes all the tests:
public static long findNb(long m) {
MathContext mc = new MathContext(64);
BigDecimal res = BigDecimal.valueOf(m).sqrt(mc);
BigDecimal d = res.multiply(BigDecimal.valueOf(8)).add(BigDecimal.ONE);
BigDecimal n = d.sqrt(mc).subtract(BigDecimal.ONE).divide(BigDecimal.valueOf(2));
return Objects.equals(n, n.setScale(0, RoundingMode.FLOOR)) ? n.longValue() : -1;
}
… and a little bonus: can you guess which of the following true
,
and which is false
?
(1646842954019151681L == 1646842954019151682D)
(1646842954019151681L == (long) 1646842954019151682D)