Как улучшить время выполнения «разделяй и властвуй»?

#python #recursion #divide-and-conquer

Вопрос:

Когда рекурсивная функция «разделяй и властвуй» не обеспечивает достаточно низкого времени выполнения, какие еще улучшения можно было бы сделать?

Допустим, например, эта power функция взята отсюда:

 def power(x, y):
    if (y == 0): return 1
    elif (int(y % 2) == 0):
        return (power(x, int(y / 2)) * power(x, int(y / 2)))
    else:
        return (x * power(x, int(y / 2)) * power(x, int(y / 2)))
 

Поскольку это рекурсивный характер memoizing and tail recursion (не уверен, может ли он быть применен наряду с разделением и завоеванием), это может существенно помочь, но я не знаю никаких других настроек. Для своих тестов я использую:

 base = 1522605027922533360535618378132637429718068114961380688657908494580122963258952897654000350692006139
exponent = 1000000
 

Нужно 301.140625s закончить, и мне все еще нужно, чтобы он мог обрабатывать большие базы и показатели… может быть, разделить проблему на более чем 2 подзадачи?

Используемый мемуаризатор можно найти здесь

Комментарии:

1. Вам нужно использовать рекурсию или использовать итеративный подход тоже хорошо ?

2. @QuentinCoumes оба хороши, хотя я заинтересован в улучшении «разделяй и властвуй». Я имею в виду улучшение рекурсивных функций.

3. Простые улучшения заключались бы в замене нескольких вызовов power(x, int(y / 2)) на уникальный с использованием переменной (даже если запоминание помогает в этом), вы также можете рассмотреть возможность использования сдвига битов вместо деления и модуля. Но не забывайте, что чистый python на самом деле не предназначен для задач, требующих больших вычислительных ресурсов.

Ответ №1:

Основная оптимизация, которую вы должны использовать здесь, — это устранение общих вложенных выражений. Рассмотрим ваш первый фрагмент кода:

 def power(x, y):
    if y == 0: 
        return 1
    elif y % 2 == 0:
        return (power(x, y // 2) * power(x, y // 2)
    else:
        return (x * power(x, y // 2) * power(x, y // 2)
 

Я немного очистил его — так y как это an int , y % 2 также будет an int , и, таким образом, нет необходимости преобразовывать типы. И канонический способ написания int(y / 2) таков y // 2 . Наконец, в if while операторах и нет необходимости заключать логическое предложение в круглые скобки, поскольку мы заканчиваем предложение точкой с запятой (тогда как в синтаксисе, подобном C, возможно наличие if оператора в 1 строку, и поэтому нам нужны круглые скобки).

Проблема здесь в том, что вы вычисляете power(x, y // 2) дважды как elif в том, так и в else другом случае. Вместо этого вы должны попытаться вычислить это значение только один раз.

 def power(x, y):
    if (y == 0): 
        return 1
    else:
        smaller_power = power(x, y // 2)
        if y % 2 == 1:
            return x * smaller_power * smaller_power
        else:
            return smaller_power * smaller_power
 

Это немедленно и значительно повышает эффективность вашего алгоритма. Вместо того, чтобы быть O(y) вовремя, улучшенная версия находится O(log y) во времени (предполагая, что мы считаем одну * операцию как 1).

Проявив немного сообразительности, мы можем придумать немного другой алгоритм:

 def power(x, y):
    if y == 0:
        return 1
    else:
        smaller_power = power(x * x, y // 2)
        return x * smaller_power if y % 2 == 1 else smaller_power
 

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

 def power_tail_helper(x, y, acc):
    """
    returns acc * power(x, y)
    """
    if y == 0:
        return acc
    else:
        new_acc = acc * x if y % 2 == 1 else acc
        return power_tail_helper(x * x, y // 2, new_acc)

def power_tail(x, y):
    return power_tail_helper(x, y, 1)
 

что, в свою очередь, может быть превращено в следующий итерационный алгоритм:

 def power_iter(x, y):
    acc = 1
    while y != 0:
        (x, y, acc) = (x * x, y // 2, acc * x if y % 2 == 1 else acc)
    return acc
 

Обратите внимание, что в идиоматическом Python мы бы вместо этого написали это как

 def power_iter(x, y):
    acc = 1
    while y:
        x, y, acc = (x * x, y // 2, acc * x if y % 2 else acc)
    return acc
 

используя тот факт, что числа автоматически преобразуются в логические значения в соответствующем контексте, при этом 0 равно False , а все остальные числа равны True .

Последний подход, который вы могли бы использовать, является более математическим. Вы можете вычислить показатель степени, используя логарифмы. Для получения точного ответа потребуется высокоточная библиотека логарифмов, поскольку base это 320-разрядное число, слишком большое для обычной 64-разрядной арифметической версии с плавающей запятой двойной точности log , чтобы быть достаточно точным. Это, вероятно, оптимальный подход, но вам придется провести больше исследований.

В любом случае, имейте в виду, что размер выходных данных будет числом, для хранения которого потребуется более 100 миллионов байт. Поэтому независимо от того, какой алгоритм вы используете, это займет некоторое время, так как даже «алгоритм» сохранения ответа в памяти и его считывания займет некоторое время.

Комментарии:

1. Ваше решение довольно потрясающее, спасибо. Я знаю о BigFloat том , что это оболочка для MPFR, но у меня возникли проблемы с ее использованием в Windows. Тем временем я приму ваше решение.

2. @loko Я забыл упомянуть, что когда вы имеете дело с огромными числами (например, 100 миллионами байтовых чисел), узким местом, вероятно, будет алгоритм умножения. Поэтому, если вы собираетесь использовать это в производстве для больших целых чисел, вам, вероятно, захочется изучить алгоритмы быстрого умножения, основанные на FFT.