7

From the comments on my answer here, the question was asked (paraphrase):

Write a Python program to find a 4 digit whole number, that when multiplied to itself, you get an 8 digit whole number whose last 4 digits are equal to the original number.

I will post my answer, but am interested in a more elegant solutions concise but easily readable solution! (Would someone new-ish to python be able to understand it?)

Thomas Tempelmann
  • 11,045
  • 8
  • 74
  • 149
Aaron N. Brock
  • 4,276
  • 2
  • 25
  • 43
  • The question itself has nothing to do with programming at all, but it's a nice one anyways, upvoted. – N.K Apr 05 '18 at 13:04
  • 1
    @xoxel I meant to imply that it should be solved programmatically... I'll edit the question to explicitly state that. – Aaron N. Brock Apr 05 '18 at 13:07
  • This actually belongs to another SE site, maybe codereview – Aemyl Apr 05 '18 at 13:11
  • @Aemyl I thought about that, but I disagree, this is a question asking how to solve an programming problem which, to me, is a good fit for StackOverflow. – Aaron N. Brock Apr 05 '18 at 13:14
  • That's a completely opinion based debat better not start it at all, it is a great/interesting question and deserve to be answered, weither or not it's implicitly related to programming is pointless ;p – N.K Apr 05 '18 at 13:17
  • 4
    It's unusual to find questions like this one but it's certainly on topic here. – Gabriel Apr 05 '18 at 13:18
  • 5
    I think this could be a better fit for [Programming Puzzles and Code Golf](https://codegolf.stackexchange.com/). – AJF Apr 05 '18 at 13:49
  • @AJFarmar I did not know about such a SE site... Well, there goes the rest of my day. But, BUT, this question wasn't supposed to be a 'puzzle' originally. (at least no more than any question is a 'puzzle'). – Aaron N. Brock Apr 05 '18 at 13:56
  • 5
    I find it interesting that somebody who knows the rules asks a question, others who also are familiar with the scope answer the question, some other people come and commend and vote up the question and the answers, and yet some others can't take it and want it removed from SO. I kinda find it disrespectful. – adrin Apr 05 '18 at 14:59
  • 1
    @OlivierMelançon I should note I changed the question to state that I wanted "readability" **after** it was put on hold, it used to say "elegant" (as seen in the strike-through). – Aaron N. Brock Apr 05 '18 at 18:06
  • Do you want solutions only in Python or any (legit, non-golfing) language? – smci Apr 05 '18 at 20:49
  • As @mathmandan shows, we can improve further to only test 2 in every 625 numbers, eliminating 99.68% of candidates – smci Apr 06 '18 at 00:59
  • So, for future reference, if I want to answer a question like this for someone just pastebin it? Or is there a way to word it to be "StackOverflow Approved"? – Aaron N. Brock Apr 10 '18 at 13:31

10 Answers10

13

Here is a 1-liner solution without any modules:

>>> next((x for x in range(1000, 10000) if str(x*x)[-4:] == str(x)), None)
9376

If you consider numbers from 1000 to 3162, their square gives you a 7 digit number. So iterating from 3163 would be a more optimized because the square should be a 8 digit one. Thanks to @adrin for such a good point.

>>> next((x for x in range(3163, 10000) if str(x*x)[-4:] == str(x)), None)
9376
Austin
  • 25,759
  • 4
  • 25
  • 48
  • 2
    technically, this answer would be wrong if there were such a number between 1000 and 3163 satisfying the condition, since it's square is 7 digits. – adrin Apr 05 '18 at 13:33
  • @adrin that's an excellent catch. Maybe iterating from `3163` is more optimized. – Austin Apr 05 '18 at 13:49
  • it is, I updated the time accordingly. – adrin Apr 05 '18 at 13:50
  • 1
    It should be noted that `3163` is the square root of `10000000`(the smallest 8 digit number), the number isn't being pulled out of thin air. – Aaron N. Brock Apr 05 '18 at 13:50
  • 3
    `3163 * 3163 = 10004569` which is the smallest is the smallest `8` digit perfect square. – Austin Apr 05 '18 at 13:56
  • 1
    With regards to optimizing the loop, it can be pointed out that the last digit has to be either 0, 1, 5, or 6, since only those multiplied by themselves result in themselves in the lower digit of the result. So that can eliminate 60% of the calculations by not trying the other low order digits (2, 3, 4, 7, 8, 9). But I don't know python, so I don't know how that might be implemented in your one liner solution. – MarkL Apr 05 '18 at 20:44
  • See my solution for how to further exclude 96% of candidates: you can optimize further by restricting the last two digits, there are only 4 legal possibilities: `set((n**2) % 100 for n in range(0,99+1) if ((n**2 % 100) == n))` is `{0, 1, 76, 25}` – smci Apr 05 '18 at 21:31
  • ...and here's the one-line version `import itertools; step = itertools.cycle((24, 1, 24, 51)); [x for x in range(3176, 10000, next(step)) if str(x*x)[-4:] == str(x) ]` – smci Apr 05 '18 at 21:43
  • So we figured out down there that the None is unecessary, there will always exist a solution, for any number of digits. – Olivier Melançon Apr 05 '18 at 21:44
6

If you are happy with using a 3rd party library, you can use numpy. This version combines with numba for optimization.

import numpy as np
from numba import jit

@jit(nopython=True)
def find_result():
    for x in range(1e7**0.5, 1e9**0.5):  
        i = x**2
        if i % 1e4 == x:
            return (x, i)

print(find_result())
# (9376, 87909376)
jpp
  • 159,742
  • 34
  • 281
  • 339
  • I clearly feel like this one is unnecessarly complicated, but it's working for sure ^^ ! – N.K Apr 05 '18 at 13:15
  • To be honest, the question is ambiguous. It says "elegant" which is subjective. If you're a programmer, you might find JIT-compilation elegant :). If not elegant, at least it's faster. – jpp Apr 05 '18 at 13:17
  • true that ^^ i didn't meant to be rude at all tho – N.K Apr 05 '18 at 13:18
  • I agree 'elegant' is subjective, but I'm not sure of a objective way to find which solution is 'best'. – Aaron N. Brock Apr 05 '18 at 13:23
  • 1
    @jpp What I really want is the most 'elegant' solution based on _my_ definition of elegance! I guess I've backed myself into a corner, the clear next step is to transfer my consciousness into a computer and create an API that everyone can optimize for. – Aaron N. Brock Apr 05 '18 at 13:38
5

[Almost] 1-liner:

from math import sqrt, ceil, floor
print(next(x for x in range(ceil(sqrt(10 ** 7)), floor(sqrt(10 ** 8 - 1))) if x == (x * x) % 10000))

printing:

9376

Timing:

%timeit next(x for x in range(ceil(sqrt(10 ** 7)), floor(sqrt(10 ** 8 - 1))) if x == (x * x) % 10000)
546 µs ± 32.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

@theausome 's answer (the shortest (character-wise)):

%timeit next((x for x in range(3163, 10000) if str(x*x)[-4:] == str(x)), None)
3.09 ms ± 119 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

@jpp 's answer (the fastest):

import numpy as np
from numba import jit

@jit(nopython=True)
def find_result():
    for x in range(1e7**0.5, 1e9**0.5):  
        i = x**2
        if i % 1e4 == x:
            return (x, i)
%timeit find_result()
61.8 µs ± 1.46 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
adrin
  • 4,511
  • 3
  • 34
  • 50
  • 1
    To me, that counts as a one liner, `math` should be part of the language. Also, I didn't think about using the `sqrt` function, in my answer I'm uselessly testing answers that's squares aren't 8 digits. – Aaron N. Brock Apr 05 '18 at 13:10
  • Where is the extra time coming from @theausomes answer? Logically they seem about the same, is it the `string` conversion? – Aaron N. Brock Apr 05 '18 at 13:29
  • `str` is one, and starting from 1000 instead of 3163 is another. – adrin Apr 05 '18 at 13:30
  • fair enough, I added the times. – adrin Apr 05 '18 at 13:48
  • 1
    You can make the first one a true one-liner: `print(next(x for x in range(__import__('math').ceil(__import__('math').sqrt(10 ** 7)), __import__('math').floor(__import__('math').sqrt(10 ** 8 - 1))) if x == (x * x) % 10000))` – Gabriel Apr 06 '18 at 20:37
5

The following solution is not as readable as other answers. But what it lacks in readability, it gains in efficiency.

The brute force approach checks every number in a given range, making it O(10^n) where n is the number of desired digits (worst if we account for multiplication and conversions).

Instead, we can build the desired numbers from right to left, adding digits as long as the generated number forms the trailing digits of its square. This provides a O(n³) algorithm (see the time complexity section at the bottom).

def get_all_numbers(n, _digits=None):
    # We are provided digits that form a number with desired properties
    digits = [] if _digits is None else _digits

    # Base case: if we attained the number of digits, return those digits
    if len(digits) >= n:
        return [int(''.join(digits))]

    solutions = []

    # Add digits from 0 to 9 and check if the new number satisfies our property
    for x in range(10):
        next_digits = [str(x)] + digits if x else ['0'] + digits
        next_number = int(''.join(next_digits))

        # If it does try adding yet another digit
        if int(str(next_number ** 2)[-len(next_digits):]) == next_number:
            solutions.extend(get_all_numbers(n, next_digits))

    # Filter out solutions with too few digits
    # Necessary as we could have prepended only zeros to an existing solution
    return [sol for sol in solutions if sol >= 10 ** (n - 1)]

def get_numbers(n, sqr_length=None):
    if sqr_length:
        return [x for x in get_all_numbers(n) if len(str(x ** 2)) == sqr_length]
    else:
        return get_all_numbers(n)

get_numbers(4, 8) # [9376]

This is not necessary for small numbers of digits, but allows to solve the problem for bigger inputs, where the brute force solution takes forever.

get_numbers(100) # Outputs in a few milliseconds

Time complexity

For a given number of digit n, there can only exist at most two solutions other than 0 and 1. And any solution is built from a solution for a smaller number of digits.

From that we conclude that despite the recursion, the algorithm takes O(n) steps to find a solution.

Now, each step has to execute some multiplication and conversions. Integer conversions are O(n²) and multiplication in Python uses Karatsuba's algorithm which is less than conversion.

Overall this yields a O(n³) time complexity.

This could be improved by using a linear integer conversion algorithm and would then provide a O(n^(1 + log(3))) complexity.

Olivier Melançon
  • 21,584
  • 4
  • 41
  • 73
  • 1
    There are at most 2 solutions for any `n`. We are looking for solutions to `k^2 = k (mod 10^n)`, or equivalently, `k*(k-1) = 0 (mod 10^n)`. This means either `k` is a multiple of `2^n` and `k-1` is a multiple of `5^n` or vice versa. By the [Chinese remainder theorem](https://en.wikipedia.org/wiki/Chinese_remainder_theorem), this is enough to pin the value of `k` down to 2 possibilities, one of which might be eliminated due to leading zeroes. – user2357112 Apr 05 '18 at 20:36
  • Also, we only need to track one candidate solution at a time, because if `k` is one candidate, the other is `(1 - k) % 10**n`. – user2357112 Apr 05 '18 at 20:39
  • @user2357112 Thank you. With that in mind, I believe the above algorithm is exactly O(n²), would that seem correct? – Olivier Melançon Apr 05 '18 at 20:45
  • I see you got a similar response on MSE (including the 0 and 1 cases that I probably should have explicitly mentioned). As for O(n^2), that's not quite right, because it seems to be neglecting the time complexity of base conversion and multiplication. – user2357112 Apr 05 '18 at 20:50
  • Conversions are quadratic? Really? – Olivier Melançon Apr 05 '18 at 21:01
  • There are faster algorithms, but Python goes for a comparatively straightforward [quadratic-time algorithm](https://github.com/python/cpython/blob/v3.6.5/Objects/longobject.c#L2176). I think [gmpy2](https://gmpy2.readthedocs.io/en/latest/) has a faster one. – user2357112 Apr 05 '18 at 21:03
  • Wait, did you multiply all the time complexities together? That's not right; you're only doing O(n) base conversions and O(n) multiplications, so it should be O(n^3), dominated by base conversion. (In any case, you're going to hit the recursion limit long before runtime becomes a problem.) – user2357112 Apr 05 '18 at 21:24
  • Oh but yes, I see what you mean, conversion dominates. Silly mistake. – Olivier Melançon Apr 05 '18 at 21:31
  • The recursion only seems to go n layers deep, and it only recurses down a finite number of straight paths instead of a tree of calls, since there are only a finite number of solutions at each layer. When you said O(n^2) steps earlier, I thought you were counting the cost of the string building or something like that. – user2357112 Apr 05 '18 at 21:35
  • The O(n²) steps comes from those O(n) path which have average depth of n/2. I think that one is correct. – Olivier Melançon Apr 05 '18 at 21:38
  • I only see 4 paths, not O(n). (These paths correspond to the 4 possibilities in the MSE answer, including `...00001` and `...00000`.) – user2357112 Apr 05 '18 at 21:44
  • @user2357112 Thanks for your help figuring out that this is indeed O(n³) – Olivier Melançon Apr 05 '18 at 21:52
5

Here's the one-line implementation, and it excludes 97.24% of candidates:

import itertools
step = itertools.cycle((24, 1, 24, 51))

[x for x in range(3176, 10000, next(step)) if str(x*x)[-4:] == str(x) ]

Call the number abcd. You can optimize by restricting the last two digits cd, there are only 4 legal possibilities, excluding 96% of candidates for cd. Similarly we only need to test 31 <= ab < 100, excluding 31% of candidates for ab. Thus we excluded 97.24%

cd_cands = set((n**2) % 100 for n in range(0,99+1) if ((n**2 % 100) == n))
cd_cands = sorted(cd_cands)
[0, 1, 25, 76]

for ab in range(31,99+1):
    for cd in cd_cands:
        n = ab*100 + cd
        if n**2 % 10**4 == n :
            print("Solution: {}".format(n))
            #break if you only want the lowest/unique solution
... 
Solution: 9376

(Sure you could squeeze that into a one-line list comprehension, but it would be ugly)

Now we can separate the multiple for-loops with the following observations: strictly we only need to start testing at the first legal candidate above 3162, i.e. 3176. Then, we increment by successively adding the steps (100-76, 1-0, 25-1, 76-25) = (24, 1, 24, 51)

import itertools
step = itertools.cycle((24, 1, 24, 51))

abcd = 3176
while abcd < 10**4:
    if abcd**2 % 10000 == abcd:
        print("Solution: {}".format(abcd))
    abcd += next(step)

and again that can be reduced to the one-liner(/two-liner) shown at top.

UPDATE: As @mathmandan shows, we can improve further to step = itertools.cycle((1, 624)), eliminating 99.68% of candidates. And start searching at 625 * 6 = 3750

smci
  • 32,567
  • 20
  • 113
  • 146
  • 1
    Nice (+1)! FYI, I believe I have a way to exclude all but 2 candidates out of every 625 (roughly 99.7%). Please see my answer at https://stackoverflow.com/a/49682351/2554867 , if interested. – mathmandan Apr 05 '18 at 22:47
  • As @mathmandan shows, we can improve further to `step = itertools.cycle((1, 624))`, **eliminating 99.68% of candidates**. And start searching at 625 * 6 = 3750. – smci Apr 06 '18 at 01:01
4

Here is a dynamic programming version:

We build from right to left, using the knowledge that for a number to square to itself, each less significant digit has to as well (On post, this is the same approach taken by @Olivier Melançon):

def dynamic(currents, tens, goal):
    if tens == goal:
        return [i for i in currents if len(str(i)) == tens]
    else:
        out = []
        for x in currents:
            for i in range(0,10):
                val = x + i *10**tens
                if val**2 % 10**(tens+1) == val:
                    out.append(val)
        currents = out
    tens +=1
    return dynamic(currents, tens, goal)

We call it with the 'current goal', the current tens, and the goal tens:

dynamic([0],0,4)
#[9376]

Works nicely on super large numbers, in less than a second:

dynamic([0],0,100)
#[3953007319108169802938509890062166509580863811000557423423230896109004106619977392256259918212890625,6046992680891830197061490109937833490419136188999442576576769103890995893380022607743740081787109376]
jeremycg
  • 24,657
  • 5
  • 63
  • 74
  • 1
    I'm glad somebody worked the same idea as I did. I'm trying to figure out the time complexity of this approach. Please see my answer. I know it is at most O(n²), but I have the feeling it is better than that. – Olivier Melançon Apr 05 '18 at 20:17
4

Simple one-liner that uses the modulo operator % instead of strings

print [x for x in range(3163, 10000) if x*x % 10000 == x]
# [9376]

The low end of the range 3163 is the smallest four digit number whose square is an eight digit number.

user3386109
  • 34,287
  • 7
  • 49
  • 68
3

The solution I came up with is:

# Loop over all 4 digit numbers
for x in range(1000, 10000):
  # Multiply x*x
  square = x*x
  # Convert to a string
  square = str(square)
  # Check if the square is 8 digits long
  if len(square) == 8:
    # Check if the last 4 digets match x
    if square.endswith(str(x)):
      # print the number and it's square
      print('x    = {}\nx**2 = {}'.format(str(x), square))

Which outputs:

x    = 9376
x**2 = 87909376
Aaron N. Brock
  • 4,276
  • 2
  • 25
  • 43
3

We only need to test 1 in 625 candidate numbers.

Either solution A:

upper_limit = 10**4
lower_limit = int(10**3.5) + 1
rem = lower_limit % 625
if rem > 0:
    lower_limit = lower_limit - rem + 625
for n in xrange(lower_limit, upper_limit, 625):
    if n % 16 in [1, 15]:
        print {1: n, 15: n+1}[n%16]
        break

or Solution B:

print (1 * (-39) * 16 + 0 * 1 * 625) % 10000

Read on for explanation.

Starting from the brute-force list-comprehension that tests all candidates:

from math import ceil

[n for n in xrange(ceil(10**3.5), 10000) if (n*n) % 10000 == n]

(The ceil rounds the square root of 10**7 up to nearest integer).

(Notice that 10000 is the first number whose square has 9 digits, and the loop will not test that number.)

... or if you'd like to early-terminate as soon as you find a solution:

for n in xrange(ceil(10**3.5), 10000):
    if (n*n) % 10000 == n:
        print n
        break

But consider: we are looking for numbers n**2 - n = n*(n-1) which are divisible by 10**4 = 2**4 * 5**4.

  • either n or n-1 is odd; so the other one would have to be divisible by the full 2**4 = 16. Similarly, you can't have both n and (n-1) be divisible by 5;
  • so we need either n or (n-1) to be divisible by 5**4 = 625.
  • if either one (n or (n-1)) is divisible by both 625 and 16, then that number is divisible by 10000. No such number has four digits, so it must be that either n or (n-1) is divisible by 625, and the other one is divisible by 16.
  • We can thus restrict our search space to looking only at multiples of 625 which have four digits; we just have to be careful to remember that the multiple of 625 might be either n or (n-1); the other one has to be divisible by 16.

So:

upper_limit = 10**4
lower_limit = ceil(10**3.5)
rem = lower_limit % 625
if rem > 0:
    lower_limit = lower_limit - rem + 625
for n in xrange(lower_limit, upper_limit, 625):
    if (n-1) % 16 == 0:
        print n
        break
    if (n+1) % 16 == 0:
        print n+1
        break

Or if you test n instead of (n-1), and combine both condition branches into n % 16 in [1, 15], and for compactness, you could print {1: n, 15: n+1}[n%16].

This is Solution A. (Also, you can certainly replace n%16 with n & 0xf if you prefer.)

But wait! All of this can in fact be done using the...

Chinese Remainder Theorem

We want to find n such that either: n = 0 (mod 625) and n - 1 = 0 (mod 16), or: n - 1 = 0 (mod 625) and n = 0 (mod 16).

So in each case we have two equations, with coprime moduli, solved for the same number n:

n = 0 (mod 625) and n = 1 (mod 16), or else n = 1 (mod 625) and n = 0 (mod 16).

Now (in both cases) we would use the Extended Euclidean Algorithm to find m1 and m2 such that 16*m1 + 625*m2 = 1. It turns out that -39*16 + 1*625 = 1, which leads to the Solution B above, from the second case. (Note: the first case would yield instead 625, whose square does end in 0625, but doesn't count as a solution.)

For completeness, here is an implementation for the Extended Euclidean Algorithm. The second and third outputs are the m1 and m2; in our case 1 and -39 in some order.

def extended_gcd(a, b):
    last_remainder, remainder = abs(a), abs(b)
    x, last_x, y, last_y = 0, 1, 1, 0
    while remainder:
        last_remainder, (quotient, remainder) = remainder, divmod(last_remainder, remainder)
        x, last_x = last_x - quotient*x, x
        y, last_y = last_y - quotient*y, y
    return last_remainder, last_x * ((-1)**(a < 0)), last_y * ((-1)**(b < 0))
smci
  • 32,567
  • 20
  • 113
  • 146
mathmandan
  • 580
  • 10
  • 17
  • This is very neat but too long and needs to be commented more concisely. – smci Apr 06 '18 at 00:35
  • As to getting the integer ceiling for 10**3.5, `from math import ceil, sqrt; ... ceil(10**3.5)` – smci Apr 06 '18 at 00:36
  • The whole sidebar about replacing multiplications with additions is unnecessary and obscures your important point; multiplication and addition of 32b integers cost the same in CPU cycles. – smci Apr 06 '18 at 00:44
  • Programmatically, your approach improves on mine to `step = itertools.cycle((1, 624))`, and start searching at 625 * 6 = 3750. – smci Apr 06 '18 at 01:01
  • @smci Thank you for the edits! – mathmandan Apr 06 '18 at 04:12
2

One liner here, just

print(9376)