Вопрос из ежедневной проблемы кодирования 210, воспроизведенный ниже:

Последовательность Коллатца в математике может быть определена следующим образом. Начиная с любого положительного целого числа:

if n is even, the next number in the sequence is n / 2
if n is odd, the next number in the sequence is 3n + 1

Предполагается, что каждая такая последовательность в конечном итоге достигает числа 1. Проверьте эту гипотезу.

Бонус: Какой вход n <= 1000000 дает самую длинную последовательность?

Мой код (обратите внимание, что он неполный):

def collatz(n):
    sequenceLength = 0
    while (n>=1):
        if (n==1):
            break # solution is found
        elif (n%2==0):
            n = n/2
            sequenceLength += 1
        else:
            n = 3*n+1
            sequenceLength += 1
    return(sequenceLength)

def longest_seq(limit):
    result = []
    for i in range(1, limit+1):
        result += [collatz(i)]
    print(result)
    return max(result)

Вопрос: В этом случае мне нужно проверить гипотезу, что я всегда буду достигать «1» для всех натуральных чисел. Тем не менее, мой код выше предполагает это, что означает, что я мог потенциально запустить бесконечный цикл.

Что такое хороший / элегантный метод для проверки гипотезы? Я думал что-то вроде кеша / массива, чтобы увидеть, повторяются ли значения «n» в начале цикла while. Если так, это предлагает бесконечный цикл. Тем не менее, я немного застрял в части синтаксиса и не совсем ясен в примерах, которые я видел до сих пор. Мне просто нужен способ: - Добавить вещи в кеш / эквивалент - Проверить, существует ли что-то в кеше / эквиваленте (и это либо дает мне правильный возврат, либо элегантный неверный ответ, который я могу использовать, не разбивая мою программу)

Большое спасибо.

7
Calvin 1 Июл 2019 в 10:54

6 ответов

Лучший ответ

Поскольку каждый раз, когда вы сталкиваетесь с определенным числом n_i, вы будете выполнять одну и ту же операцию, вы знаете, что, если вы встретите число, которое вы уже видели, вы будете зацикливаться бесконечно.

Один из способов решить эту проблему - сохранить последовательность. Затем вы можете проверить на каждом шаге, что вы еще не встретили номер. Вот как это может выглядеть:

def collatz(n):
    sequence = []
    while (n>=1):
        if n in sequence:
            break
        else:
            sequence.append(n)
            if (n==1):
                break # solution is found
            elif (n%2==0):
                n = n/2
            else:
                n = 3*n+1
    return(sequence)

Примечание . Вы можете использовать набор вместо массива, если хотите, чтобы код выполнялся быстрее, поскольку у массивов больше время поиска (кредит @tobias_k). Но вы потеряете точный порядок вашей последовательности.

6
vlemaistre 1 Июл 2019 в 09:49
cache = {}
def collatz(n):
    sequenceLength = 0
    while (n>=1):
        if n in cache:  # number already encountered
            return sequenceLength + cache[n]
        if (n==1):
            break # solution is found
        elif (n%2==0):
            n = n/2
            sequenceLength += 1
        else:
            n = 3*n+1
            sequenceLength += 1
    return sequenceLength

def longest_seq(limit):
    result = []
    for i in range(1, limit+1):
        c = collatz(i)
        result.append(c)
        cache[i] = c  # put the answer in the cache

    print(result)
    return max(result)
0
Zefick 1 Июл 2019 в 08:06

Рекурсия + кеширование:

cache = {1:0}
def collatz(n):
    if n in cache:
       return cache[n]
    else:
       if n%2==0:
          m = n//2
       else:
          m = 3*n+1
       res = collatz(m) + 1
       cache[n] = res
       return res


def longest_seq(limit):
    result = []
    for i in range(1, limit+1):
        result += [collatz(i)]
    return max(result)

Затем работает:

r =  longest_seq(1000000)
#524

Найдите значение, соответствующее максимуму:

[x for x,y in cache.items() if y==r]
#[837799]
2
nicola 1 Июл 2019 в 08:27

Для этого в python есть встроенный декоратор: lru_cache. Но сначала вы должны использовать рекурсивную функцию, чтобы извлечь выгоду из декораторов.

from functools import lru_cache

@lru_cache()
def collatz(n):
    sequenceLength = 0
    if n == 1:
        return n
    elif n % 2 == 0:
        return 1 + collatz(n // 2)
    else:  # n % 2 == 1:
        return 1 + collatz(3 * n + 1)

Здесь вы можете проверить, как он работает, и изменить его, чтобы сделать то, что вы хотите: https: // docs.python.org/3/library/functools.html

Вы хотите проверить, был ли вход уже виден ранее, вы можете определить свой собственный декоратор:

def deco(f):
    cache = {}
    @wraps(f)
    def wrapper(*args, **kwargs):
        if 'r' in kwargs and kwargs['r']:  # reset the cache when first called
            cache.clear()
        try:                                                                                                    
            res = cache[args]  
            # We have already seen these parameters !
            print('cache hit', *args)
            if res is None:
                raise KeyError
        except KeyError:
            cache[args] = None  # temporary store a value here
            res = f(*args)
            cache[args] = res  # final value stored
        return res

    return wrapper

Вам просто нужно изменить определение collatz:

@deco
def collatz(n, /):  # force positional argument here
    # ... (same code here)

И назовите это со сбросом:

collatz(10, r=True)
>>> 7

Но, как сказал Тобиас, этот код никогда не должен запускаться для функции collatz

3
pLOPeGG 1 Июл 2019 в 09:48

Как уже отмечалось, вы можете использовать functools.lru_cache добавить кеширование практически к любой функции (с хешируемыми параметрами), но в ее текущей форме кеширование не сильно поможет вашей функции collatz, так как вы вызываете ее только один раз для любого параметра. Таким образом, у вас есть много кэшированных значений, но вы никогда не используете ни одно из них. Вместо этого вы можете изменить свою функцию с итеративной на рекурсивную, чтобы использовать кэширование:

@functools.lru_cache()
def collatz(n):
    if n <= 1:
        return 0
    if n % 2 == 0:
        return 1 + collatz(n // 2)
    else:
        return 1 + collatz(3*n + 1)

Таким образом, вы можете повторно использовать вычисленный один раз результат в последующем вызове collatz, если вы объединяетесь в «ветку», которую вы уже исследовали в предыдущем вызове (есть несколько хороших графиков, например, в статья в Википедии гипотезы.

Обратите внимание, что в зависимости от размера вашего ввода, вы можете столкнуться с пределом рекурсии. Чтобы это исправить, вы можете использовать sys.setrecursionlimit(somelargenumber) увеличить предел рекурсии, но, конечно, это работает только до некоторой точки.


Однако, похоже, это не то, о чем вы на самом деле просили (а также не значительно ускоряет код для поиска самой длинной последовательности до 1 000 000). Вместо этого кажется, что вы хотите проверить, не нашли ли вы «цикл» во время одного вызова функции collatz. Здесь lru_cache не поможет. Вместо этого вы должны просто добавить set из уже seen значений n и посмотреть, находится ли текущий номер в этом наборе. Тем не менее, я бы не ожидал, что этот код будет запущен ...

def collatz(n):
    sequenceLength = 0
    seen = set()
    while (n>=1):
        if n in seen:
            print("BREAKING NEWS! COLLATZ CONJECTURE DISPROVEN!")
            break
        seen.add(n)
        # remainder of your code
0
tobias_k 1 Июл 2019 в 09:45

Из-за гипотезы Коллатца, всегда заканчивающейся 1, я не вижу необходимости делать кэшированную проверку эквивалентности. Но дополнительная проверка никогда не повредит, поэтому вы можете просто выполнить простейшую троичную проверку n1 = n if n1 != n else exit(), назначив переменные n1 = 0 для первого раунда ранее.

0
Ajama 1 Июл 2019 в 08:16