Search code examples
pythonalgorithmfilteroeis

Fastest way to filter the A010784 sequence


The A010784 sequence at OEIS is the sequence containing only numbers with distinct digits. This is a finite amount.

What I've been trying to do is find several numbers in this sequence with certain attributes.
E.g: 6 is a distinct number of magnitude 10. This can be found as follows:

  • 6 x 1 = 6
  • 6 x 2 = 12
  • 6 x 3 = 18
  • 6 x 4 = 24
  • 6 x 5 = 30
  • 6 x 6 = 36
  • 6 x 7 = 42
  • 6 x 8 = 48
  • 6 x 9 = 54
  • 6 x 10 = 60
  • 6 x 11 = 66 (Two sixes; these aren't both distinct digits.)

Now I'm trying the highest numbers available for several magnitudes of order.
Let's say all orders from 1 up to 20.

What I'm currently doing is loop from the highest possible distinct number, which is 9,876,543,210 and the highest unique number of magnitude 1, down to a very low number.
This method feels extremely inefficient. At least, to me it does.

I'm pretty sure I'm missing some stuff about factoring that should be able to make things a lot faster.

def unique_num(num):
    # Check whether number is unique.
    return Boolean

def unique_num_magnitude(num):
    # Check of which magnitude the unique number is
    return Integer

# Dictionary with the current highest values
# for each magnitude.
values = {1: 987654321...}

# Set limits
upper_limit = 9876543210
lower_limit = 1

for numbers in range(upper_limit, lower_limit, -1):
    unique = unique_num(num) # Boolean
    if (unique):
        magnitude = unique_num_magnitude(num)
        if (values[magnitude] < num):
            values[magnitude] = num

Solution

  • Sure there is better way. You should build the numbers bottom up, i.e. if a number a1...ak (these are k digits) is not of magnitude N so that a digit appears twice within the first k digits of the result for any multiplicand 2..N then neither is any number b1...bm a1...ak. This provides a categorically faster recursive enumeration procedure because it cuts an exponential amount of search space away. Details left as an exercise to OP.

    Longer explanation:

    Assume there is a procedure

     magnitude(number_str)
    

    that calculates the magnitude for the number number_str represented in 10-base, so that the procedure returns 0 if number_str contains at least one double occurrence of a digit, 1 if number_str has every digit distinct but multiplying the number by two results in a digit occurring multiple times, etc.

    This procedure can be implemented in terms of another one

     unique_digits(number_str)
    

    which returns true if all digits in number_str are unique, otherwise false.

    Now then magnitude can be implemented as

     magnitude(number_str):
       n = str_to_int(number_str)
       k = n
       i = 0
       while (unique_digits(int_to_str(k))):
         k = k + n
         i = i + 1
       return i
    

    Now this magnitude procedure can be changed to a nocarry_magnitude that changes the check subtly:

     nocarry_magnitude(number_str, limit):
       l = length(number_str)
       assert (l > 0)
       n = str_to_int(number_str)
       k = n
       i = 0
       while (unique_digits(right_substring(int_to_str(k), l))):
         k = k + n
         i = i + 1
         if (i == limit):
           break
       return i
    

    This procedure checks for the multiple times occurring digits only in as many rightmost digits (lowest-order digits) of the product in the loop as there were digits in the original input. A limit parameter is needed to make sure the procedure terminates as it's possible that the procedure runs in an infinite loop otherwise. Clearly for any string s it holds that

     magnitude(s) <= nocarry_magnitude(s, M) for large M
    

    For instance, take number 19:

     19 * 1 = 19
     19 * 2 = 38
     19 * 3 = 57
     19 * 4 = 76
     19 * 5 = 95
     19 * 6 = 114 // magnitude("19") = 5
     19 * 7 = 133 // nocarry_magnitude("19", 100) = 6
    

    What I wrote above in the short description is that if

     nocarry_magnitude(s, x) == k     for x > k
    

    then for any string that's obtained by postfixing s (denote this by x + s below), it holds that

     x : string of digits
     magnitude(x + s) <= nocarry_magnitude(x + s, m) <= nocarry_magnitude(s, m)
     when m >= magnitude(x + s)
    

    The first inequality comes from the claim above and the second one is obvious from the definition of nocarry_magnitude. Note that magnitude(x + s) <= magnitude(s) is a non-theorem in general as exemplified by

     magnitude( "56")  = 1  // 56 * 2 = 112
     magnitude("256")  = 12 // the first duplicate occurs at 3328
    

    which is caused by the carry. nocarry_magnitude ignores the carry which is why the suffix of a string has always as large nocarry-magnitude as any extension of it (towards left, i.e. higher-order digits).

    Now then you can write a significantly faster recursive procedure as this for searching for numbers with magnitude at least M:

     search(str):
       if (str != ""):
         int n = nocarry_magnitude(str, M)
         if (n < M):
           return      # the optimization
         int k = magnitude(str)
         if (k >= M):
           store_answer(str)
       for d in ['1', '2', '3', '4', '5', '6', '7', '8', '9',
                 '10', '20', '30', '40', '50', '60', '70', '80', '90']:
         search(d + str)
    
     search("")
    

    Here's a full Python implementation (searching for magnitude 36):

    def unique_digits(s):
        r = (len(list(s)) == len(set(s)))
        return r
    
    def magnitude(s):
        n = int(s)
        k = n
        assert n > 0
        i = 0
        while (unique_digits(str(k))):
            k = k + n
            i = i + 1
        return i
    
    def nocarry_magnitude(s, limit):
        l = len(s)
        assert l > 0
        n = int(s)
        assert n > 0
        k = n
        i = 0
        while (unique_digits(str(k)[-l:])):
            k = k + n
            i = i + 1
            if (i >= limit):
                break
        return i
    
    M = 36
    
    def search(s):
        if (not(s == "")):
            n = nocarry_magnitude(s, M)
            if (n < M):
                return
            k = magnitude(s)
            if (k >= M):
                print "Answer: %s |" % s,
                i = int(s)
                k = i
                n = 1
                while (n <= M):
                    print k,
                    k = k + i
                    n = n + 1
                print
        for d in ['1', '2', '3', '4', '5', '6', '7', '8', '9',
                  '10', '20', '30', '40', '50', '60', '70', '80', '90']:
            search(d + s)
    
    search("")
    

    And here's a run that takes less than one second; this finds the answer '27' which seems to be the unique number that provides the record magnitude 36:

    Answer: 27 | 27 54 81 108 135 162 189 216 243 270 297 324 351 378 405
                 432 459 486 513 540 567 594 621 648 675 702 729 756 783
                 810 837 864 891 918 945 972