Tricky Double

Other languages: Español 한국어 Português 中文

Repost icon

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)
all posts ->