2

A Python coding exercise asks to make a function f such that f(k) is the k-th number such that its k-th digit from the left and from the right sums to 10 for all k. For example 5, 19, 28, 37 are the first few numbers in the sequence.

I use this function that explicitly checks if the number 'n' satisfies the property:

def check(n):

    #even digit length
    if len(str(n)) % 2 == 0:

        #looping over positions and checking if sum is 10
        for i in range(1,int(len(str(n))/2) + 1):

            if int(str(n)[i-1]) + int(str(n)[-i]) != 10:

                return False

    #odd digit length
    else:

        #checking middle digit first
        if int(str(n)[int(len(str(n))/2)])*2 != 10:

            return False

        else:
            #looping over posotions and checking if sum is 10
            for i in range(1,int(len(str(n))/2) + 1):

                if int(str(n)[i-1]) + int(str(n)[-i]) != 10:

                    return False

    return True

and then I loop over all numbers to generate the sequence:

for i in range(1, 10**9):

    if check(i):
        print(i)

However the exercise wants a function f(i) that returns the i-th such number in under 10 seconds. Clearly, mine takes a lot longer because it generates the entire sequence prior to number 'i' to calculate it. Is it possible to make a function that doesn't have to calculate all the prior numbers?

Mike
  • 444
  • 1
  • 8
  • 19
  • Given the `k`-th digit at the left side has value *d*, what do you know about the value of the `k`-th value at the right? – Willem Van Onsem Jul 21 '19 at 18:33
  • 3
    You should add your task definition into your question, because it is not clear what you are asking. For example, numbers 55, 5555, 555555, etc. meet your conditions and it is not clear what other constraints are there. – niekas Jul 21 '19 at 18:39
  • @WillemVanOnsem Thanks, that should help me optimize what I have now. Finding an explicit rule for the n-th digit eludes me still. – Mike Jul 21 '19 at 18:39
  • @TomZych Thanks, I corrected that. I am looking for numbers whose digits have the given property. – Mike Jul 21 '19 at 18:42
  • 1
    @niekas for example 5, 19, 28, 37 are the first few numbers in the sequence. – Mike Jul 21 '19 at 18:42
  • Your question is not quite clear. Do you want a generator that generates that sequence (your first paragraph), or do you a function `f(i)` that returns the `i`th such number (your last paragraph)? It seems to be the latter but it is not crystal clear. Also, is `f(0)` the first number in the sequence (`5`) or is `f(1)`? – Rory Daulton Jul 21 '19 at 19:01
  • @RoryDaulton The latter. I think f(1) being the first number is better. Implicitly I guess I have the function that generates the i-th such number, but I have to calculate everything up to that point to retrieve it. – Mike Jul 21 '19 at 19:10
  • @RoryDaulton I've changed the phrasing of my first sentence to reflect the difference. – Mike Jul 21 '19 at 19:13
  • @Mike Between 0 and 9, there are 9^0 = 1 numbers. Between 10 and 99, there are 9^1 = 9. There are again 9^1 = 9 numbers between 100 and 999. Then it jumps to 9^2 = 81 numbers between 1000 and 9999, etc. You can exploit the fact that the count of valid numbers between each power of 10 is a power of 9 to know where (i.e., which power of 10) to start looking without having to evaluate all previous numbers. – brentertainer Jul 21 '19 at 19:13

2 Answers2

2

Testing every natural number is a bad method. Only a small fraction of the natural numbers have this property, and the fraction decreases quickly as we get into larger numbers. On my machine, the simple Python program below took over 3 seconds to find the 1,000th number (2,195,198), and over 26 seconds to find the 2,000th number (15,519,559).

# Slow algorithm, only shown for illustration purposes

# '1': '9', '2': '8', etc.
compl = {str(i): str(10-i) for i in range(1, 10)}

def is_good(n):
    # Does n have the property
    s = str(n)
    for i in range((len(s)+1)//2):
        if s[i] != compl.get(s[-i-1]):
            return False
    return True

# How many numbers to find before stopping
ct = 2 * 10**3

n = 5
while True:
    if is_good(n):
        ct -= 1
        if not ct:
            print(n)
            break
    n += 1

Clearly, a much more efficient algorithm is needed.

We can loop over the length of the digit string, and within that, generate numbers with the property in numeric order. Sketch of algorithm in pseudocode:

for length in [1 to open-ended]:
    if length is even, middle is '', else '5'
    half-len = floor(length / 2)
    for left in (all 1) to (all 9), half-len, without any 0 digits:
        right = 10's complement of left, reversed
        whole-number = left + middle + right

Now, note that the count of numbers for each length is easily computed:

Length    First    Last     Count
1         5        5        1
2         19       91       9
3         159      951      9
4         1199     9911     81
5         11599    99511    81

In general, if left-half has n digits, the count is 9**n.

Thus, we can simply iterate through the digit counts, counting how many solutions exist without having to compute them, until we reach the cohort that contains the desired answer. It should then be relatively simple to compute which number we want, again, without having to iterate through every possibility.

The above sketch should generate some ideas. Code to follow once I’ve written it.

Code:

def find_nth_number(n):
    # First, skip cohorts until we reach the one with the answer
    digits = 1
    while True:
        half_len = digits // 2
        cohort_size = 9 ** half_len
        if cohort_size >= n:
            break
        n -= cohort_size
        digits += 1

    # Next, find correct number within cohort

    # Convert n to base 9, reversed
    base9 = []
    # Adjust n so first number is zero
    n -= 1
    while n:
        n, r = divmod(n, 9)
        base9.append(r)
    # Add zeros to get correct length
    base9.extend([0] * (half_len - len(base9)))
    # Construct number
    left = [i+1 for i in base9[::-1]]
    mid = [5] * (digits % 2)
    right = [9-i for i in base9]
    return ''.join(str(n) for n in left + mid + right)

n = 2 * 10**3

print(find_nth_number(n))
Tom Zych
  • 13,329
  • 9
  • 36
  • 53
  • Thanks! I'm working on this myself, I'll post an answer once I get to it, still working on parsing your pseudo-code, would love to see the code if you come up with it. – Mike Jul 21 '19 at 19:26
  • This looks like it generalizes the get_starting_point function from my answer, and does it more efficiently to boot! Nice work. If I might make a suggestion, at the end intialize `result = 0`, then do `for digit in left + right + mid: result = (10 * result) + digit`. I think it's computationally faster than joining as a string and then you also get the result as an integer. – brentertainer Jul 21 '19 at 20:38
  • @brentertainer: I try not to worry about efficiency for any subtask that’s executed once and that takes less than 10 ms either way. As for getting an integer, the output is the same either way, and remember that the integer would have to be converted back to a string for output anyway. – Tom Zych Jul 21 '19 at 20:53
1

This is a function that exploits the pattern where the number of "valid" numbers between adjacent powers of 10 is a power of 9. This allows us to skip over very many numbers.

def get_starting_point(k):
    i = 0
    while True:
        power = (i + 1) // 2
        start = 10 ** i
        subtract = 9 ** power
        if k >= subtract:
            k -= subtract
        else:
            break
        i += 1
    return k, start

I combined this with the method you've defined. Supposing we are interested in the 45th number, this illustrates the search starts at 1000, and we only have to find the 26th "valid" number occurring after 1000. It is guaranteed to be less than 10000. Of course, this bound gets worse and worse at scale, and you would want to employ the techniques suggested by the other community members on this post.

k = 45
new_k, start = get_starting_point(k)
print('new_k: {}'.format(new_k))
print('start at: {}'.format(start))
ctr = 0
for i in range(start, 10**9):
    if check(i):
        ctr += 1
        if ctr == new_k:
            break
print(i)

Output:

new_k: 26
start at: 1000
3827

It seems the 45th number is 3827.

brentertainer
  • 2,118
  • 1
  • 6
  • 15