0

I am trying to complete the following exercise: https://www.codewars.com/kata/whats-a-perfect-power-anyway/train/python

I tried multiple variations, but my code breaks down when big numbers are involved (I tried multiple variations with solutions involving log and power functions):

Exercise:
Your task is to check wheter a given integer is a perfect power. If it is a perfect power, return a pair m and k with m^k = n as a proof. Otherwise return Nothing, Nil, null, None or your language's equivalent.

Note: For a perfect power, there might be several pairs. For example 81 = 3^4 = 9^2, so (3,4) and (9,2) are valid solutions. However, the tests take care of this, so if a number is a perfect power, return any pair that proves it.

The exercise uses Python 3.4.3

My code:

import math
def isPP(n):
    for i in range(2 +n%2,n,2):
            a = math.log(n,i)
            if int(a) == round(a, 1):
                if pow(i, int(a)) == n:
                    return [i, int(a)]
    return None

Question: How is it possible that I keep getting incorrect answers for bigger numbers? I read that in Python 3, all ints are treated as "long" from Python 2, i.e. they can be very large and still represented accurately. Thus, since i and int(a) are both ints, shouldn't the pow(i, int(a)) == n be assessed correctly? I'm actually baffled.

JohnnyQ
  • 425
  • 4
  • 16
  • 1
    Can you provide examples of such incorrect answers for bigger numbers? – lucasnadalutti Sep 18 '16 at 18:04
  • `int(a)` is an `int`, but `a` isn't and is already inaccurate. Anyway, **demonstrate the problem**. Just *"my code breaks down"* and *"getting incorrect answers"* isn't exactly helpful. – Stefan Pochmann Sep 18 '16 at 18:04
  • @lucasnadalutti 4913000 is a perfect power – JohnnyQ Sep 18 '16 at 18:08
  • Your code doesn't work: `NameError: name 'log' is not defined` – Stefan Pochmann Sep 18 '16 at 18:10
  • @ Stefan Pochmann I get an inaccurate result. Each time I submit my code it's a different one, but e.g. now I run it and I get "6434856 is a perfect power", which my function evaluated as "None" – JohnnyQ Sep 18 '16 at 18:10
  • Try now, for some reason the codewars website did not require me to write math.log but accepted "log" – JohnnyQ Sep 18 '16 at 18:13
  • @Łukasz It's not floating point math, it's int math - my code includes a check which is "Btw., before returning the values, try and see if using them actually gets you the correct answer". For that, it uses int values and thinks that it gets a correct result, but it does not. – JohnnyQ Sep 18 '16 at 18:16
  • If you want it to work on large integers, you will have to ditch `math.log` entirely and use a factoring-based approach. – John Coleman Sep 18 '16 at 18:18
  • You should really just check the computations step by step with an example like your 4913000. Very easy to see where it gets wrong and why. – Stefan Pochmann Sep 18 '16 at 18:19
  • 1
    Factoring is overkill, a binary search seems like it will work: http://cstheory.stackexchange.com/q/2077 – John Coleman Sep 18 '16 at 18:29
  • 1
    You should use [an IDE](http://sopython.com/wiki/Python_IDEs) unless you already use one. Then run your code in debug mode. For example if you are using PyCharm follow [these instructions](https://www.youtube.com/watch?v=QJtWxm12Eo0) . – user Sep 18 '16 at 18:30
  • 1
    Throw out both `if` statements, and replace them with `if pow(i, round(a)) == n:`. In Python 3 (but not Python 2), 1-argument `round()` returns an integer, so the power will be exact. As is, your `round(a, 1)` returns a float, and there's no reason at all to imagine that the code will work. For example, if due to floating-point vagaries, the mathematical (infinitely precise) log is 3 but is _computed_ as 2.999999999999, `int()` of that will be 2 but `round(that, 1)` will be 3.0. `round(that)` will return the correct 3. – Tim Peters Sep 18 '16 at 18:37

1 Answers1

3

(edit note: also added integer nth root bellow)

you are in the right track with logarithm but you are doing the math wrong, also you are skipping number you should not and only testing all the even number or all the odd number without considering that a number can be even with a odd power or vice-versa

check this

>>> math.log(170**3,3)
14.02441559235585
>>> 

not even close, the correct method is described here Nth root

which is:

let x be the number to calculate the Nth root, n said root and r the result, then we get

rn = x

take the log in any base from both sides, and solve for r

logb( rn ) = logb( x )

n * logb( r ) = logb( x )

logb( r ) = logb( x ) / n

blogb( r ) = blogb( x ) / n

r = blogb( x ) / n

so for instance with log in base 10 we get

>>> pow(10, math.log10(170**3)/3 )
169.9999999999999
>>> 

that is much more closer, and with just rounding it we get the answer

>>> round(169.9999999999999)
170
>>> 

therefore the function should be something like this

import math

def isPP(x):
    for n in range(2, 1+round(math.log2(x)) ):
        root   = pow( 10, math.log10(x)/n )
        result = round(root)
        if result**n == x:
            return result,n

the upper limit in range is to avoid testing numbers that will certainly fail

test

>>> isPP(170**3)
(170, 3)
>>> isPP(6434856)
(186, 3)
>>> isPP(9**2)
(9, 2)
>>> isPP(23**8)
(279841, 2)
>>> isPP(279841)
(529, 2)
>>> isPP(529)
(23, 2)
>>> 

EDIT

or as Tin Peters point out you can use pow(x,1./n) as the nth root of a number is also expressed as x1/n

for example

>>> pow(170**3, 1./3)
169.99999999999994
>>> round(_)
170
>>> 

but keep in mind that that will fail for extremely large numbers like for example

>>> pow(8191**107,1./107)
Traceback (most recent call last):
  File "<pyshell#90>", line 1, in <module>
    pow(8191**107,1./107)
OverflowError: int too large to convert to float
>>> 

while the logarithmic approach will success

>>> pow(10, math.log10(8191**107)/107)
8190.999999999999
>>> 

the reason is that 8191107 is simple too big, it have 419 digits which is greater that the maximum float representable, but reducing it with a log produce a more reasonable number

EDIT 2

now if you want to work with numbers ridiculously big, or just plain don't want to use floating point arithmetic altogether and use only integer arithmetic, then the best course of action is to use the method of Newton, that the helpful link provided by Tin Peters for the particular case for cube root, show us the way to do it in general alongside the wikipedia article

def inthroot(A,n):
    if A<0:
        if n%2 == 0:
            raise ValueError
        return - inthroot(-A,n)
    if A==0:
        return 0
    n1 = n-1
    if A.bit_length() < 1024: # float(n) safe from overflow
        xk = int( round( pow(A,1/n) ) )
        xk = ( n1*xk + A//pow(xk,n1) )//n  # Ensure xk >= floor(nthroot(A)).
    else:
        xk = 1 << -(-A.bit_length()//n)  # power of 2 closer but greater than the nth root of A
    while True:
        sig = A // pow(xk,n1)
        if xk <= sig:
            return xk
        xk = ( n1*xk + sig )//n

check the explanation by Mark Dickinson to understand the working of the algorithm for the case of cube root, which is basically the same for this

now lets compare this with the other one

>>> def nthroot(x,n):
        return pow(10, math.log10(x)/n )

>>> n = 2**(2**12) + 1  # a ridiculously big number
>>> r = nthroot(n**2,2)
Traceback (most recent call last):
  File "<pyshell#48>", line 1, in <module>
    nthroot(n**2,2)
  File "<pyshell#47>", line 2, in nthroot
    return pow(10, math.log10(x)/n )
OverflowError: (34, 'Result too large')
>>> r = inthroot(n**2,2)
>>> r == n
True
>>> 

then the function is now

import math

def isPPv2(x):
    for n in range(2,1+round(math.log2(x))):
        root = inthroot(x,n)
        if root**n == x:
            return root,n

test

>>> n = 2**(2**12) + 1  # a ridiculously big number
>>> r,p = isPPv2(n**23)
>>> p
23
>>> r == n
True
>>> isPPv2(170**3)
(170, 3)
>>> isPPv2(8191**107)
(8191, 107)
>>> isPPv2(6434856)
(186, 3)
>>>

now lets check isPP vs isPPv2

>>> x = (1 << 53) + 1
>>> x
9007199254740993
>>> isPP(x**2)
>>> isPPv2(x**2)
(9007199254740993, 2)
>>>     

clearly, avoiding floating point is the best choice

Community
  • 1
  • 1
Copperfield
  • 8,131
  • 3
  • 23
  • 29
  • Hey, thanks for the helpful answer, it worked and your point on logarithms was excellent! I used the step 2 because I'm pretty sure that if n = p^k is an even number, then p must be even as well, because if you keep multiplying odd numbers by each other you will keep getting only odd numbers. – JohnnyQ Sep 18 '16 at 19:53
  • with n=p^k, yes if n is even p must be even, but that tell you nothing about k and that is the one you are searching for. So skipping all the odd/even when n is one or the other neglect the case when k is the opposed, as is illustrated by 170^3 = 4913000, that is a even number, and by skipping all the odd numbers obviously you will never get the right answer for k – Copperfield Sep 18 '16 at 20:09
  • Just noting that `pow( 10, math.log10(x)/n )` is more simply (and accurately) expressed as `x**(1./n)` or as `pow(x, 1./n)` (same thing under the covers). Especially in extreme cases, the library `pow()` is much more accurate than slinging distinct `log` and exponentiation steps yourself each in native float precision; But for extremely large integers, a float may not be able to hold even an approximation to the root - in that case you need an all-integer root function. – Tim Peters Sep 18 '16 at 20:52
  • @TimPeters for extremely big integers that will explode with a OverflowError. – Copperfield Sep 18 '16 at 21:40
  • Yes, it will. And for even bigger integers than that, so will `pow(10, math.log10(x)/n)` - when the root itself exceeds the dynamic range of a float. Then you need something like this instead: http://stackoverflow.com/a/35276426 – Tim Peters Sep 18 '16 at 22:01
  • @TimPeters good point, I added a pure integer version with the help of that link – Copperfield Sep 19 '16 at 01:39
  • 1
    @JohnnyQ I also added a alternative version – Copperfield Sep 19 '16 at 02:19
  • 2
    Good! Note that the all-integer version isn't _just_ saving us from overflow problems - it's also necessary if the root has more than 53 bits (floats only have 53 bits of precision). For example, say `x = (1 << 53) + 1`. Then the integer version should correctly determine that `x**2` is the square of `x`, but no float version possibly can, since `x` isn't exactly representable as a float. This despite that `x**2` is only 107 bits wide, and at about 8.1e31 is nowhere near overflowing. Nice job :-) – Tim Peters Sep 19 '16 at 02:25