Approximate floating point number with n-digit precision

9

We have a floating point number r between 0 and 1, and an integer p.

Find the fraction of integers with the smallest denominator, which approximates r with at least p-digit precision.

  • Inputs: r (a floating point number) and p (integer).
  • Outputs: a and b integers, where
    • a/b (as float) approximates r until p digits.
    • b is the possible smallest such positive integer.

For example:

  • if r=0.14159265358979 and p=9,
  • then the result is a=4687 and b=33102,
  • because 4687/33102=0.1415926530119026.

Any solution has to work in theory with arbitrary-precision types, but limitations caused by implementations' fixed-precision types do not matter.

Precision means the number of digits after "0." in r. Thus, if r=0.0123 and p=3, then a/b should start with 0.012. If the first p digits of the fractional part of r are 0, undefined behavior is acceptable.

Win criteria:

  • The algorithmically fastest algorithm wins. Speed is measured in O(p).
  • If there are multiple fastest algorithms, then the shortest wins.
  • My own answer is excluded from the set of the possible winners.

P.s. the math part is actually much easier as it seems, I suggest to read this post.

peterh - Reinstate Monica

Posted 2017-09-16T23:45:53.810

Reputation: 347

Answers

7

JavaScript, O(10p) & 72 bytes

r=>p=>{for(a=0,b=1,t=10**p;(a/b*t|0)-(r*t|0);a/b<r?a++:b++);return[a,b]}

It is trivial to prove that the loop will be done after at most O(10p) iterations.

f=
r=>p=>{for(a=0,b=1,t=10**p;(a/b*t|0)-(r*t|0);a/b<r?a++:b++);return[a,b]}
<math xmlns="http://www.w3.org/1998/Math/MathML">
  <mrow>
    <mn>0.<input type="text" id="n" value="" oninput="[p,q]=f(+('0.'+n.value))(n.value.length);v1.value=p;v2.value=q;d.value=p/q" /></mn>
    <mo>=</mo>
    <mfrac>
      <mrow><mn><output id="v1">0</output></mn></mrow>
      <mrow><mn><output id="v2">1</output></mn></mrow>
    </mfrac>
    <mo>=</mo>
    <mn><output id="d">0</output></mn>
  </mrow>
</math>

Many thanks to Neil's idea, save 50 bytes.

tsh

Posted 2017-09-16T23:45:53.810

Reputation: 13 072

Why are you fiddling around with padEnd and match? Can't you just slice each string to the correct length and then subtract them? – Neil – 2017-09-18T09:14:00.663

@Neil Sorry I hadn't catch your point. The added padEnd is used for testcase f(0.001,2) and f(0.3,2). – tsh – 2017-09-18T09:23:39.557

I was thinking you could simplify down to something along the lines of (r,p)=>{for(a=0,b=1;`${a/b}`.slice(0,p+2)-`${r}`.slice(0,p+2);a/b<r?a++:b++);return[a,b]} (not fully golfed). – Neil – 2017-09-18T09:27:49.270

@Neil 120 -> 70 bytes. :) – tsh – 2017-09-18T09:56:48.393

Whoa, that's much better! – Neil – 2017-09-18T09:58:05.970

4

Haskell, O(10p) in worst case 121 119 bytes

g(0,1,1,1)
g(a,b,c,d)r p|z<-floor.(*10^p),u<-a+c,v<-b+d=last$g(last$(u,v,c,d):[(a,b,u,v)|r<u/v])r p:[(u,v)|z r==z(u/v)]

Try it online!

Saved 2 bytes thanks to Laikoni

I used the algorithm from https://math.stackexchange.com/questions/2432123/how-to-find-the-fraction-of-integers-with-the-smallest-denominator-matching-an-i.

At each step, the new interval is one half of the previous interval. Thus, the interval size is 2**-n, where n is the current step. When 2**-n < 10**-p, we are sure to have the right approximation. Yet if n > 4*p then 2**-n < 2**-(4*p) == 16**-p < 10**-p. The conclusion is that the algorithm is O(p).

EDIT As pointed out by orlp in a comment, the claim above is false. In the worst case, r = 1/10**p (r= 1-1/10**p is similar), there will be 10**p steps : 1/2, 1/3, 1/4, .... There is a better solution, but I don't have the time right now to fix this.

jferard

Posted 2017-09-16T23:45:53.810

Reputation: 1 764

I know code golf is only the secondary goal, but you can drop the f= and save two bytes with z<-floor.(*10^p),u<-a+c,v<-b+d. – Laikoni – 2017-09-18T17:53:32.920

@Laikoni I did not count the two bytes. I don't know how to remove f= on TIO in Haskell code. – jferard – 2017-09-18T18:04:12.297

You can add the -cpp compiler flag and write f=\ in the header: Try it online!

– Laikoni – 2017-09-18T18:10:02.507

"At each step, the new interval is one half of the previous interval." How do you know this? The first step is 1/2, yes, but then the next step is for example the mediant of 1/2 and 1/1 giving 2/3, which is not halving the interval. – orlp – 2017-09-18T21:05:50.517

@orlp You're absolutelty right. I was far too optimistic and the complexity is O(10^p) in the worst case. I have a better solution but don't have the time to write it right now. – jferard – 2017-09-19T18:34:00.723

@orlp It is right, but if it is going pseudo-randomly, in a roughly level distribution, then it doesn't affect the algorithmic complexity. – peterh - Reinstate Monica – 2017-09-20T10:32:38.877

@peterh You aren't very convincing with that argument - prove it. But there is a bigger issue: what exactly is a 'roughly level distribution over the rationals'? Think about it for a second, it doesn't make sense. How would you pick a random rational number between 0 and 1? Complexity should absolutely be measured in the worst case here. – orlp – 2017-09-20T10:58:17.927

@orlp Sorry I had to mention that I am talking about my opinion. However, random selected elements of infinite sets is a well defined thing in the mathematics, although not in the very elemental one. Whole libraries could be filled with the literature of random walks, stochastic processes, distributions (in the mathematical sense), these are heavily used also in the engineering practice, control theory or in the quantum mechanics. – peterh - Reinstate Monica – 2017-09-20T11:49:59.020

@orlp Algorithmical complexity can be measured both for the worst case and as mean, I think it depends on the circumstances which should be used in the actual context. For example, the add/remove algorithms of balanced trees typically have a logarithmical upper limit for the worst case. If we estimate the speed of an algorithm in a specific application, then calculating for the mean speed may be more common (particularly if we know, that the input will be "friendly", i.e. it won't be tuned against us and the time limits are not very hard). Like the seek time of hard disks, it is a mean value. – peterh - Reinstate Monica – 2017-09-20T11:56:10.433

@peterh I'm familiar with all this, this is a bit condescending. My point is that there is no uniform random distribution of the rational numbers, so there's no good way to measure 'mean' complexity. Whatever your complexity measure is depends entirely on your distribution. So, my question is: if you insist on 'mean' complexity, then what is your distribution? Without this information the only possibility is to use worst-case complexity. If you can't describe the function used to select a random rational r between 0 and 1 you can't speak of an average complexity. – orlp – 2017-09-20T14:28:33.927

0

C, 473 bytes (without context), O(p), non-competing

This solution uses the math part detailed in this excellent post. I calculated only calc() into the answer size.

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

void calc(float r, int p, int *A, int *B) {
  int a=0, b=1, c=1, d=1, e, f;
  int tmp = r*pow(10, p);
  float ivl = (float)(tmp) / pow(10, p);
  float ivh = (float)(tmp + 1) / pow(10, p);

  for (;;) {
    e = a + c;
    f = b + d;

    if ((ivl <= (float)e/f) && ((float)e/f <= ivh)) {
      *A = e;
      *B = f;
      return;
    }

    if ((float)e/f < ivl) {
      a = e;
      b = f;
      continue;
    } else {
      c = e;
      d = f;
      continue;
    }
  }
}

int main(int argc, char **argv) {
  float r = atof(argv[1]);
  int p = atoi(argv[2]), a, b;
  calc(r, p, &a, &b);
  printf ("a=%i b=%i\n", a, b);
  return 0;
}

peterh - Reinstate Monica

Posted 2017-09-16T23:45:53.810

Reputation: 347

It also nears the probably fastest possible solution in the sense of cpu cycles, at least on conventional machines. – peterh - Reinstate Monica – 2017-09-24T14:43:27.247