Number of consecutive zeros in the decimal representation of a double

余生颓废 提交于 2019-11-29 11:54:18

[Short version: The answer is 20. Recast the problem in terms of finding good rational approximations to numbers of the form 2^e / 10^d; then use continued fractions to find the best such approximation for each suitable d and e.]

The answer appears to be 20: that is, there are examples of IEEE 754 binary64 floats whose decimal expansion has 20 consecutive zeros, but there are none with 21 consecutive zeros in their decimal expansion (excluding leading and trailing zeros). The same is true for strings of nines.

For the first part, all I need to do is exhibit such a float. The value 0x1.9527560bfbed8p-1000 is exactly representable as a binary64 float, and its decimal expansion contains a string of 20 zeros:

1.4770123739081015758322326613397693800319378788862225686396638475789157389044026850930817635789180868803699741668118826590044503912865915000931065333265410967343958956370955236330760696646247901278074331738806828003156818618589682432778455224012594723731303304343292224317331720902511661748324604219378419442700000000000000000000740694966568985212687104794747958616712153948337746429554804241586090095019654323133732729258896166004754316995632195371041441104566613036026346868128222593894931067078171989365490315525401375255259854894072456336393577718955037826961967325532389800834191597056333066925969522850884268136311674777047673845172073566950098844307658716553833345849153012040436628485227928616281881622762650607683099224232137203216552734375E-301

For the part of the question about nines, the decimal expansion of 0x1.c23142c9da581p-405 contains a string of 20 nines:

2.12818792307269553358078502102171540639252016258831784842556110831434197718043638405555406495645619729155240037555858106390933161420388023706431461384056688295540725831155392678607931808851292893574214797681879999999999999999999941026584542575391157788777223962620780080784703190447744595561259568772261019375946489162743091583251953125E-122

To explain how I found the numbers above, and to show that there are no examples with 21 consecutive zeros, we'll need to work a bit harder. A real number with a long string of 9s or 0s in its decimal expansion has the form (a + eps)*10^d for some integers a and d and real number eps, with a nonzero (we might as well assume a positive) and eps nonzero and small. For example, if 0 < abs(eps) < 10^-10 then a + eps has at least 10 zeros following the decimal point (if eps is positive), or 10 nines following the decimal point (if eps is negative); multiplying by 10^d allows for shifting the location of the string of zeros or nines.

But we're interested in numbers of the above form that are simultaneously representable as an IEEE 754 binary64 float; in other words, numbers that are also of the form b*2^e for integers b and e satisfying 2^52 <= b <= 2^53, with e limited in range (and with some additional restrictions on b once we get into the subnormal range, but we can worry about that later).

So combining this, we're looking for solutions to (a + eps) * 10^d = b * 2^e in integers a, b, d and e such that eps is small, a is positive and 2^52 <= b <= 2^53 (and we'll worry about ranges for d and e later). Rearranging, we get eps / b = 2^e / 10^d - a / b. In other words, we're looking for good rational approximations to 2^e / 10^d, with limited denominator. That's a classic application of continued fractions: given d and e, one can efficiently find the best rational approximation with denominator bounded by 2^53.

So the solution strategy in general is:

for each appropriate d and e:
    find the best rational approximation a / b to 2^e / 10^d with denominator <= 2^53
    if (the error in this rational approximation is small enough):
        # we've got a candidate
        examine the decimal expansion of b*2^e

We have only around 2 thousand values for e to check, and at worst a few hundred d for each such e, so the whole thing is computationally very feasible.

Now to details: what does "small enough" mean? Which d and e are "appropriate"?

As to "small enough": let's say that we're looking for strings of at least 19 zeros or nines, so we're looking for solutions with 0 < abs(eps) <= 10^-19. So it's enough to find, for each d and e, all a and b such that abs(2^e / 10^d - a / b) <= 10^-19 * 2^-52. Note that because of the limit on b there can be only one such fraction a / b; if there were another such a' / b' then we have 1 / 2^106 <= 1 / (b *b') <= abs(a / b - a' / b') <= 2 * 10^-19 * 2^-52, a contradiction. So if such a fraction exists it's necessarily the best rational approximation with the given denominator bound.

For d and e: to cover the binary64 range including subnormals, we want e to range from -1126 to 971 inclusive. If d is too large then 2^e / 10^d will be much smaller than 2^-53 and there's no hope of a solution; d <= 16 + floor(e*log10(2)) is a practical bound. If d is too small (or too negative) then 2^e / 10^d will be an integer and there's no solution; to avoid that, we want d > min(e, 0).

With all that covered, let's write some code. The Python solution is pretty straightforward, thanks in part to the existence of the Fraction.limit_deminator method, which does exactly the job of finding the best rational approximation within limits.

from fractions import Fraction
from itertools import groupby
from math import floor, log10

def longest_run(s, c):
    """Length of the longest run of a given character c in the string s."""
    runs = [list(g) for v, g in groupby(s, lambda k: k == c) if v]
    return max(len(run) for run in runs) if runs else 0

def closest_fraction(d, e):
    """Closest rational to 2**e/10**d with denominator at most 2**53."""
    f = Fraction(2**max(e-d, 0) * 5**max(-d, 0), 2**max(0, d-e) * 5**max(0, d))
    approx = f.limit_denominator(2**53)
    return approx.numerator, approx.denominator

seen = set()
emin = -1126
emax = 971
for e in range(emin, emax+1):
    dmin = min(e, 0) + 1
    dmax = int(floor(e*log10(2))) + 16
    for d in range(dmin, dmax+1):
        num, den = closest_fraction(d, e)
        x = float.fromhex('0x{:x}p{}'.format(den, e))
        # Avoid duplicates.
        if x in seen:
            continue
        seen.add(x)
        digits = '{:.1000e}'.format(x).split('e')[0].replace('.','').strip('0')
        zero_run = longest_run(digits, '0')
        if zero_run >= 20:
            print "{} has {} zeros in its expansion".format(x.hex(), zero_run)
        nine_run = longest_run(digits, '9')
        if nine_run >= 20:
            print "{} has {} nines in its expansion".format(x.hex(), nine_run)

There's plenty of scope for performance improvements there (not using Python's fractions module would be a good start :-); as it stands, it takes a few minutes to run to completion. And here are the results:

0x1.9527560bfbed8p-1000 has 20 zeros in its expansion
0x1.fa712b8efae8ep-997 has 20 zeros in its expansion
0x1.515476ae79b24p-931 has 20 nines in its expansion
0x1.a5a9945a181edp-928 has 20 nines in its expansion
0x1.86049d3311305p-909 has 20 zeros in its expansion
0x1.69c08f3dd8742p-883 has 20 zeros in its expansion
0x1.1b41d80091820p-861 has 20 zeros in its expansion
0x1.62124e00b5e28p-858 has 20 zeros in its expansion
0x1.ba96e180e35b2p-855 has 20 zeros in its expansion
0x1.31c5be6377c48p-786 has 20 zeros in its expansion
0x1.7e372dfc55b5ap-783 has 20 zeros in its expansion
0x1.7e89dc1c3860ap-555 has 20 nines in its expansion
0x1.7e89dc1c3860ap-554 has 20 nines in its expansion
0x1.7e89dc1c3860ap-553 has 20 nines in its expansion
0x1.7e89dc1c3860ap-552 has 20 nines in its expansion
0x1.30bd91ea994cbp-548 has 20 zeros in its expansion
0x1.4a5f9de9ee064p-468 has 20 nines in its expansion
0x1.9cf785646987dp-465 has 20 nines in its expansion
0x1.c23142c9da581p-408 has 20 nines in its expansion
0x1.c23142c9da581p-407 has 20 nines in its expansion
0x1.c23142c9da581p-406 has 20 nines in its expansion
0x1.c23142c9da581p-405 has 20 nines in its expansion
0x1.ba431f4e34be9p+738 has 20 nines in its expansion

Obviously, it is at least 15, as illustrated by this Smalltalk/Squeak code:

1.0 successor asTrueFraction printShowingMaxDecimalPlaces: 100.
-> '1.0000000000000002220446049250313080847263336181640625'

1.0 predecessor asTrueFraction printShowingMaxDecimalPlaces: 100.
-> '0.99999999999999988897769753748434595763683319091796875'

Now, it's a bit more involved to demonstrate that there can't be more than 15 consecutive zeros. You search a floating point number f=(A*10^n+B)*10^p where

  • n > 15
  • A is integer
  • 0 < B < 1

But the float must also be expressed as integer significand and biased exponent f=s*2^e where

  • s is integer
  • 0 < s < 2^53 < 5^23

We thus have: s=(A*2^n*5^n+B)*2^(p-e)*5^p with s < 2^53.

My first answer was false, this is to be finished...

In all generality, s can be written, s=2^a*5^b+c, c not divisible by 2 nor 5.

A*2^(n+p-e)*5^(n+p)+B*2^(p-e)*5^p = 2^a*5^b+c

We can search a construct with A=1, B=c*2^(e-p)/5^p<1, n+p-e=a, n+p=b, e-p=n-a.

B=c*2^(n-a)/5^(b-n)

I tried all pairs a,b, such that 2^52 < 2^a*5^b < 2^53, but could not find any n>15 satisfying B<1... Trying with A > 1 will only make things worse (it involves reducing a and b).

Thus I don't think that there is any double with 16 consecutive zeros, but it's not a beautiful demonstration...

I don't have a solution to this, but here's an approach I might follow:

  • Keep track of the longest string of zeroes found so far. Call the length L. It's at least 15 long, thanks to aka.nice's answer.
  • For each possible exponent and each location 10^k where L+1 consecutive zeroes could occur, you get a messy little knapsack problem.
  • Compute the relevant 53 powers of two modulo 10^{k+L}, and round them so that they have about L+1 significant figures.
  • Find all combinations of the first 26 powers of two modulo 10^{k+L} using native integer math.
  • Find all combinations of the last 26 powers of two modulo 10^{k+L} similarly.
  • Sort both halves, and look for pairs that give you something very close to the negative of the implied bit using a linear scan. You probably only get a few matches doing this.
  • Check each match using sprintf or something.

Seems like you have to run this loop a few million times, but it should be doable on a few dozen modern computers in a few dozen hours.

I'd also add that there's an integer exactly representable in floating point that has a lot of trailing zeroes: 87960930222080000000000000000000000 has 22 trailing zeroes and is 10F0CF064DD5920000000000000000 in hex. (In fact, 10^22 is exactly representable as a double and it obviously has 22 trailing zeroes. You can't do any better since 5^23 would need to divide such a significand and that's impossible. Oh well.)

Kamlesh Kumar Yadav

This code will remove 0 in the end of decimal.

 private void button1_Click(object sender, EventArgs e)
        {
            String Str = textBox1.Text;
            String lstVal = GetDecimalString(Str);
           textBox2.Text = lstVal;
        }

    private static string GetDecimalString(String Str)
    {
        String lstVal = "";
        if (Str.IndexOf(".") > -1)
        {
            String[] Last = Str.Split('.');
            if (Last.Length == 1)
            {
                lstVal = Last[0];
            }
            else
            {
                lstVal = Last[1];
            }
            String TrimedData = lstVal;
            for (int i = lstVal.Length - 1; i >= 0; i--)
            {
                if (TrimedData.EndsWith("0") && TrimedData.Length > 3)
                {
                    TrimedData = TrimedData.Substring(0, i - 1);
                }
            }
            lstVal = TrimedData;

            if (lstVal.Length < 3)
                lstVal = lstVal.PadRight(3, '0');

            lstVal = String.Join(".", Last[0], lstVal);
        }
        else
        {
            lstVal = Str;
        }
        return lstVal;
    }

The pragmatic solution is:

  • call your existing function to return a string containing just the number of decimal places that you need;
  • convert the result back to a double-precision value;
  • if this value is greater than the original, decrement the final digit.
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!