BHIL 2024 - MiniMath

6 minute read

Challenge description:

In math, the way is often as important as the result - in this case: the shorter, the better!

import math
import string

def devisors(n):
    '''Generates all devisors of n, except n itself.'''
    assert n < 10_000_000, 'Too big number.'

    for i in range(1, n):
        if n % i == 0:
            yield i


def next_number(n):
    '''Returns the sum of all devisors of n.'''
    return sum(devisors(n))


def get_cycle(n):
    '''
    Generates all numbers in the cycle of n.
    NOTE: Be careful with infinite loops. Use only sociable numbers.
    '''
    c = next_number(n)
    l = 0
    while c != n:
        c = next_number(c)
        yield c
        l += 1
        if l > 100:
            raise ValueError('Infinite loop detected.')


def scrumble(n):
    n = str(n)

    res = ''
    while len(n) > 4:
        res += n[2:4] + n[0]
        n = n[4:]
    
    return int(res + n)


def main():
    shortest = 1000
    while True:
        try:
            eq = input('Please enter an equation: ')
            assert set(eq) & set(string.digits) == set(), 'No numbers allowed.'

            n = eval(eq, {name: getattr(math, name) for name in dir(math)})
            #print(f'{n=}')
            #print(f'{scrumble(n)=}')
            res = list(get_cycle(scrumble(n)))
            #print(f'{res=}')
            assert len(res) >= 3, 'Not a sociable number.'

            print('Length:', len(eq))
            if len(eq) < shortest:
                shortest = len(eq)
                print('New Best!')
            else:
                print('Can you make it a bit shorter please?')
                print('Best so far:', shortest)
        except Exception as e:
            # print the exception and continue
            print(e)


if __name__ == '__main__':
    main()

First assert - ‘No numbers allowed.’

This challenge includes one Python script. When we run the script, it asks for an “equation.” We can provide any equation we want, but it has one strict condition: the equation cannot contain any numbers! Here are some examples of valid equations:

len("aaa")

len("aaa") * len("aaaaa")

pow(len("aa"), len("aaa")) 
  • Note: Sometimes, the script may throw the exception Infinite loop detected. This exception is raised by the function get_cycle when it cannot find the number it is searching for. This function searches for a number such that the sum of the divisors of one of its divisors equals the sum of the divisors of the number itself.

Second assert - ‘Not a sociable number.’

Second Assertion - “Not a sociable number.” This assertion occurs when the function get_cycle returns fewer than three numbers in the res list. To find a number that makes the function get_cycle return three or more numbers, we use brute force, iterating from 1 to 10,000,000.

import math
import string

#First result: 101916

dvd = {}

def devisors(n):
    '''Generates all devisors of n, except n itself.'''
    assert n < 10_000_000, '' #'Too big number.'

    for i in range(1, n):
        if n % i == 0:
            yield i


def next_number(n):
    '''Returns the sum of all devisors of n.'''
    if n not in dvd.keys():
        dvd[n] = sum(devisors(n))
    return dvd[n]


def get_cycle(n):
    '''
    Generates all numbers in the cycle of n.
    NOTE: Be careful with infinite loops. Use only sociable numbers.
    '''
    c = next_number(n)
    l = 0
    while c != n:
        c = next_number(c)
        yield c
        l += 1
        if l > 100:            
            raise ValueError('Infinite loop detected.')


def scrumble(n):
    n = str(n)

    res = ''
    while len(n) > 4:
        res += n[2:4] + n[0]
        n = n[4:]
    
    return int(res + n)


def main():
    shortest = 1000
    
    for i in range(10_000_000):
        try:
            eq = 'len("' + "a" * i + '")'
            assert set(eq) & set(string.digits) == set(), 'No numbers allowed.'

            n = eval(eq, {name: getattr(math, name) for name in dir(math)})
            res = list(get_cycle(scrumble(n)))
            assert len(res) >= 3, 'Not a sociable number.'        

            print(i)

        except Exception as e:
            pass


if __name__ == '__main__':
    main()

Congrats! Now we have a number that can pass all the tests in the challenge and proceed to the final condition. Note: You can try this by creating a Python expression that generates the number 101916 without directly using numbers. The following script generates a Python expression that passes all challenge tests up to the final condition:

print('len("%s")' % ("a" * 101916))

The final condition

This condition checks if the length of the Python line we provide is less than the value stored in a variable called shortest. This variable has the value 1000, so we need to write a shorter Python line to pass the condition. To achieve this, we can use multiplication to create larger numbers using smaller strings, instead of relying on a single len() function. For example:

# This line ret 25
#------------------
len("aaaaaaaaaaaaaaaaaaaaaaaaa")

# Use this
# This line ret 25
#-------------------
len("aaaaa")*len("aaaaa")

Now our line is shorter and simpler, to make it even shorter, we can use the pow() function:

# This line ret 125
#------------------
len("aaaaa")*len("aaaaaaaaaaaaaaaaaaaaaaaaa")

# Use this
# This line ret 125
#-------------------
pow(len("aaaaa"),len("aaa"))

The next script calculates the shortest possible Python line that produces the number 101916 and satisfies the challenge’s final condition.

import math

#First result: 101916

LEN_LINE = 'len("%s")'
POW_LINE = 'pow(%s,%s)'
BASE = 4

def getn(n = 4):
    """
    This function return numbers string by using 'len()' func.
    """
    return (LEN_LINE % ("a" * n))

def getp(a,b):
    """
    This function return numbers string by using 'pow()' func.
    """
    return(POW_LINE % (getn(a),getn(b)))

def getans(num, calc_method):
    """
    This function get the calc method and the number that the user
    want to calc, then it will get the dummy numbers (not real numbers, the numbers is functions output)
    and return python line that calc the user input number.
    """
    res = ""

    # Run until the line can calc all the user number
    while num != 0:
        # Add new part for the calc line
        out = calc_method(num)
        num = out[1]        

        # Check if the line is already fill with something or not
        if len(res):
            res += "+" + out[0]
        else:
            res = out[0]
        
    return res


def calc_using_len(n):
    """
    This function return python line code with 'len()' functions that return number.
    """
    crr = 1
    res = ""
    if n <= 4: # Check if the number is too small
        return (getn(n), 0)
    
    # Create python line using 'len()' functions to calc the number
    while n >= crr * 4:
        crr *= 4
        res += getn() + "*"

    # Remove the last '*' sign
    res = res[:-1]

    # Return tuple of the python line and the rest of the number
    return (res, n - crr)

def calc_using_pow(n):
    """
    This function return python line code with 'len()' functions that return number.
    """
        
    res = ""

    if n <= 4: # Check if the number is too small
        return (getn(n), 0)
    
    ans = int(math.log(n,BASE)) # Get the base number for the pow action

    res = getp(BASE, ans) # Get pow python line string

    # Return the python line and the rest of the number
    return (res, n - pow(BASE, ans))


def main():
    num = int(input("num: ")) # User input number

    # Init the pow calc method by default
    calc_method = calc_using_pow

    # Default number
    if num == 0:
        num = 101916

    # Get user option for Calc method
    op = input("1. calc using len\n2. calc using pow\noption: ")
    if op == "1":
        calc_method = calc_using_len

    # Get output of python calc line for the user number
    res = getans(num, calc_method)

    # Profit
    print("int(" + res + ")")

if __name__ == "__main__":
    main()