3

We want to compare a^b to c^d, and tell if the first is smaller, greater, or equal (where ^ denotes exponentiation).

Obviously, for very large numbers, we cannot explicitely compute these values.

The most common approach in this situation is to apply log on both sides and compare b * log(a) to d * log(c). The issue here is that logs are floating-point operations, and as such we cannot trust our answer with 100% confidence (there might be some values which are incredibly close, and because of floating-point error we get a wrong answer).

Is there an algorithm for solving this problem? I've been scouring the intrernet for this, but I can only find solutions which work for particular cases only (e.g. in which one exponent is a multiple of another), or which use floating point in some way (logarithms, division) etc.

Mark Dickinson
  • 29,088
  • 9
  • 83
  • 120
Nu Nunu
  • 376
  • 3
  • 14
  • Is computing the logs in higher-than-IEEE-double precision not an option? – David Eisenstat Jan 13 '21 at 16:54
  • Can you give examples of typical inputs? (Your title says "order of 1 billion", but it's not clear to me exactly which numbers are on the order of 1 billion. Does that refer to `a`, `b`, `c` and `d` individually, or to the powers `a^b` and `c^d`, or ...?) – Mark Dickinson Jan 13 '21 at 18:40
  • There is rational arithmetic, which is notably supported by Python and Ruby (`fractions` module and `Rational` class, respectively). – Peter O. Jan 13 '21 at 20:34

1 Answers1

1

This is sort of two questions in one:

  1. Are they equal?

  2. If not, which one is greater?

As Peter O. observes, it's easiest to build in a language that provides an arbitrary-precision fraction type. I'll use Python 3.

Let's assume without loss of generality that a ≤ c (swap if necessary) and b is relatively prime to d (divide both by the greatest common divisor).

To get at the core of the question, I'm going to assume that a, c > 0 and b, d ≥ 0. Removing this assumption is tedious but not difficult.

Equality test

There are some easy cases where a = 1 or b = 0 or c = 1 or d = 0.

Separately, necessary conditions for a^b = c^d are

i. b ≥ d, since otherwise b < d, which together with a ≤ c implies a^b < c^d;

ii. a is a divisor of c, since we know from (i) that a^b = c^d is a divisor of c^b = c^(b−d) c^d.

When these conditions hold, we can divide through by a^d to reduce the problem to testing whether a^(b−d) = (c/a)^d.

In Python 3:

def equal_powers(a, b, c, d):
    while True:
        lhs_is_one = a == 1 or b == 0
        rhs_is_one = c == 1 or d == 0
        if lhs_is_one or rhs_is_one:
            return lhs_is_one and rhs_is_one
        if a > c:
            a, b, c, d = c, d, a, b
        if b < d:
            return False
        q, r = divmod(c, a)
        if r != 0:
            return False
        b -= d
        c = q


def test_equal_powers():
    for a in range(1, 25):
        for b in range(25):
            for c in range(1, 25):
                for d in range(25):
                    assert equal_powers(a, b, c, d) == (a ** b == c ** d)


test_equal_powers()

Inequality test

Once we've established that the two quantities are not equal, it's time to figure out which one is greater. (Without the equality test, the code here could run forever.)

If you're doing this for real, you should consult an actual reference on computing elementary functions. I'm just going to try to do the simplest thing that works.

Time for a calculus refresher. We have the Taylor series

−log x = (1−x) + (1−x)^2/2 + (1−x)^3/3 + (1−x)^4/4 + ...

To get a lower bound, truncate the series. To get an upper bound, we can truncate but replace the final term (1−x)^n/n with (1−x)^n/n (1/x), since

(1−x)^n/n (1/x)
= (1−x)^n/n (1 + (1−x) + (1−x)^2 + ...)
= (1−x)^n/n + (1−x)^(n+1)/n + (1−x)^(n+2)/n + ...
> (1−x)^n/n + (1−x)^(n+1)/(n+1) + (1−x)^(n+2)/(n+2) + ...

To get a good convergence rate, we're going to want 0.5 ≤ x < 1, which we can achieve by dividing x by a power of two.

In Python, we'll represent a real number as an infinite generator of shrinking intervals that contain the true value. Once the intervals for b log a and d log c are disjoint, we can determine how they compare.

import fractions


def minus(x, y):
    while True:
        x_lo, x_hi = next(x)
        y_lo, y_hi = next(y)
        yield x_lo - y_hi, x_hi - y_lo


def times(b, x):
    for lo, hi in x:
        yield b * lo, b * hi


def restricted_log(a):
    series = 0
    n = 0
    numerator = 1
    while True:
        n += 1
        numerator *= 1 - a
        series += fractions.Fraction(numerator, n)
        yield -(series + fractions.Fraction(numerator * (1 - a), (n + 1) * a)), -series


def log(a):
    n = 0
    while a >= 1:
        a = fractions.Fraction(a, 2)
        n += 1
    return minus(restricted_log(a), times(n, restricted_log(fractions.Fraction(1, 2))))


def less_powers(a, b, c, d):
    lhs = times(b, log(a))
    rhs = times(d, log(c))
    while True:
        lhs_lo, lhs_hi = next(lhs)
        rhs_lo, rhs_hi = next(rhs)
        if lhs_hi < rhs_lo:
            return True
        if rhs_hi < lhs_lo:
            return False


def test_less_powers():
    for a in range(1, 10):
        for b in range(10):
            for c in range(1, 10):
                for d in range(10):
                    if a ** b != c ** d:
                        assert less_powers(a, b, c, d) == (a ** b < c ** d)


test_less_powers()
David Eisenstat
  • 64,237
  • 7
  • 60
  • 120