1

Given a non-negative integer c, I need an efficient algorithm to find the largest integer x such that

x*(x-1)/2 <= c

Equivalently, I need an efficient and reliably accurate algorithm to compute:

x = floor((1 + sqrt(1 + 8*c))/2)        (1)

For the sake of defineteness I tagged this question C++, so the answer should be a function written in that language. You can assume that c is an unsigned 32 bit int.

Also, if you can prove that (1) (or an equivalent expression involving floating-point arithmetic) always gives the right result, that's a valid answer too, since floating-point on modern processors can be faster than integer algorithms.

a06e
  • 18,594
  • 33
  • 93
  • 169
  • What is the expected range of `c`? – phs Oct 01 '14 at 20:12
  • @phs You can assume `c` is a 32 bit unsigned integer. – a06e Oct 01 '14 at 20:17
  • Thank you. One more question: how efficient is "efficient"? You've already found the `O(1)` closed form (I assume, I haven't actually run your coefficients yet through the quad. formula.) Are you looking for bit twiddling hacks? – phs Oct 01 '14 at 20:20
  • @phs as efficient as possible, as long as it is not completely unredable. If by `O(1)` closed form you mean the expression `floor((1 + sqrt(1 + 8*c))/2)`, note that it involves floating-point arithmetic, which can be inaccurate. I haven't checked whether in this case it always produces the right result. – a06e Oct 01 '14 at 20:29
  • 2
    Well, for the first one rearrange the relation so that you have `x` on the left side, i.e. solve it mathematically. Then, think about how to write the according code. Anyhow, what have you tried yourself so far? – Ulrich Eckhardt Oct 01 '14 at 20:33
  • @UlrichEckhardt I know that the solution is `x = floor((1 + sqrt(1 + 8*c))/2)` (I wrote it in the question), and I know how to program this in C++. The problem is that it involves floating-point arithmetic and I am not sure whether that will always be accurate for all possible values of `c` – a06e Oct 01 '14 at 20:38
  • @EvilTeach So far I have tried using the formula `x = floor((1 + sqrt(1 + 8*c))/2)`. It appears to give accurate results for the values of `c` that I have tried. But I would like to see a proof/guarantee that this formula (or an equivalent version of it) always gives accurate results. I can also conceive a bissection method to find `x`, but I didn't bother to program it since I suspect there are better solutions. – a06e Oct 01 '14 at 20:40
  • It gives the correct result because the quadratic formula has been proven to give the correct result. – AndyG Oct 01 '14 at 20:43
  • @AndyG You're saying that the digits before the decimal point are always right for this formula, for all integers `c`? Do you have a reference where I can find a proof? – a06e Oct 01 '14 at 20:45
  • Can you spare the memory (and the CPU cycles at start up time) to build a lookup table? If so, note that `x(x-1)/2` is simply the sum of the first `x-1` natural numbers. You could build a lookup table - with all entries if `c` is never too big, but if `c` may take the entire 32-bit range, you might be better with a decent integer square root implementation, or just deal with the floating point (which isn't nearly as slow as it used to be). – twalberg Oct 01 '14 at 20:45
  • @twalberg The performance of floating-point in this case is fast enough for me. I am only worried about accuracy. Do I have a guarantee that the expression (1) will always give the right result in floating-point arithmetic? – a06e Oct 01 '14 at 20:46
  • @becko: Yes, you will always get the correct result PLUS some error from truncation and accumulation of computing `sqrt`. – AndyG Oct 01 '14 at 20:48
  • If you're worried about the accuracy of `sqrt`, it will be more than enough for 32-bit integers. See http://stackoverflow.com/a/4319151/5987 – Mark Ransom Oct 01 '14 at 21:06
  • feels like a math question rather than a C++ question but sure. – Ahmed Masud Oct 02 '14 at 05:50
  • @AhmedMasud If it were a math question I would not care about floating-point arithmetic in C++, which is the main issue here. – a06e Oct 02 '14 at 12:25

3 Answers3

5

If you're willing to assume IEEE doubles with correct rounding for all operations including square root, then the expression that you wrote (plus a cast to double) gives the right answer on all inputs.

Here's an informal proof. Since c is a 32-bit unsigned integer being converted to a floating-point type with a 53-bit significand, 1 + 8*(double)c is exact, and sqrt(1 + 8*(double)c) is correctly rounded. 1 + sqrt(1 + 8*(double)c) is accurate to within one ulp, since the last term being less than 2**((32 + 3)/2) = 2**17.5 implies that the unit in the last place of the latter term is less than 1, and thus (1 + sqrt(1 + 8*(double)c))/2 is accurate to within one ulp, since division by 2 is exact.

The last piece of business is the floor. The problem cases here are when (1 + sqrt(1 + 8*(double)c))/2 is rounded up to an integer. This happens if and only if sqrt(...) rounds up to an odd integer. Since the argument of sqrt is an integer, the worst cases look like sqrt(z**2 - 1) for positive odd integers z, and we bound

z - sqrt(z**2 - 1) = z * (1 - sqrt(1 - 1/z**2)) >= 1/(2*z)

by Taylor expansion. Since z is less than 2**17.5, the gap to the nearest integer is at least 1/2**18.5 on a result of magnitude less than 2**17.5, which means that this error cannot result from a correctly rounded sqrt.

Adopting Yakk's simplification, we can write

(uint32_t)(0.5 + sqrt(0.25 + 2.0*c))

without further checking.

David Eisenstat
  • 64,237
  • 7
  • 60
  • 120
1

If we start with the quadratic formula, we quickly reach sqrt(1/4 + 2c), round up at 1/2 or higher.

Now, if you do that calculation in floating point, there can be inaccuracies.

There are two approaches to deal with these inaccuracies. The first would be to carefully determine how big they are, determine if the calculated value is close enough to a half for them to be important. If they aren't important, simply return the value. If they are, we can still bound the answer to being one of two values. Test those two values in integer math, and return.

However, we can do away with that careful bit, and note that sqrt(1/4 + 2c) is going to have an error less than 0.5 if the values are 32 bits, and we use doubles. (We cannot make this guarantee with floats, as by 2^31 the float cannot handle +0.5 without rounding).

In essense, we use the quadratic formula to reduce it to two possibilities, and then test those two.

uint64_t eval(uint64_t x) {
  return x*(x-1)/2;
}
unsigned solve(unsigned c) {
  double test = sqrt( 0.25 + 2.*c );
  if ( eval(test+1.) <= c )
    return test+1.
  ASSERT( eval(test) <= c );
  return test;
}

Note that converting a positive double to an integral type rounds towards 0. You can insert floors if you want.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
1

This may be a bit tangential to your question. But, what caught my attention is the specific formula. You are trying to find the triangular root of Tn - 1 (where Tn is the nth triangular number).

I.e.:

Tn = n * (n + 1) / 2

and

Tn - n = Tn - 1 = n * (n - 1) / 2

From the nifty trick described here, for Tn we have:

n = int(sqrt(2 * c))

Looking for n such that Tn - 1 ≤ c in this case doesn't change the definition of n, for the same reason as in the original question.

Computationally, this saves a few operations, so it's theoretically faster than the exact solution (1). In reality, it's probably about the same.

Neither this solution or the one presented by David are as "exact" as your (1) though.

Exact vs int(sqrt(2 * c))

floor((1 + sqrt(1 + 8*c))/2) (blue) vs int(sqrt(2 * c)) (red) vs Exact (white line)


Exact vs int(sqrt(0.25 + 2 * c) + 0.5)

floor((1 + sqrt(1 + 8*c))/2) (blue) vs int(sqrt(0.25 + 2 * c) + 0.5 (red) vs Exact (white line)

My real point is that triangular numbers are a fun set of numbers that are connected to squares, pascal's triangle, Fibonacci numbers, et. al.

As such there are loads of identities around them which might be used to rearrange the problem in a way that didn't require a square root.

Of particular interest may be that Tn + Tn - 1 = n2

I'm assuming you know that you're working with a triangular number, but if you didn't realize that, searching for triangular roots yields a few questions such as this one which are along the same topic.

Community
  • 1
  • 1
Seth
  • 45,033
  • 10
  • 85
  • 120