Рубрики
Без рубрики

Решение проблемы «сломанный калькулятор» сложно

Я обычно не делаю проблемы с лецкодом … Хотя мне действительно нравится кодировать, и часто ма … Помечено с Python, LeetCode, Puzzle, шаблон.

Хотя я действительно наслаждаюсь кодировкой, и часто занимаюсь хорошим использованием этих навыков как часть моей 9-5 рабочих мест, я никогда не провел много проблем с решением времени на лецкоде или подобных сайтах. Мне нравятся мыслительные упражнения, но не «необходимыми» опытом, чтобы получить преимущество в интервью программного обеспечения (хотя, хотя это должно ли это на самом деле дать кому-то край, крайне обсуждаемо), и когда у меня есть время, чтобы сесть и писать Какой-то код обычно решать непосредственную практическую проблему. Ничего из этого не значит, что я не могу или не любил, чтобы быть в состоянии сделать это, это просто факт жизни для меня.

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

Примечание. Вам, вероятно, придется подписаться на учетную запись с LeetCode, чтобы увидеть проблему на их веб-сайте. Если вы не хотите делать это, просто продолжайте читать – я подведу его ниже.

Головоломка была, как кажется, часто бывает так, один, который выглядел легко, прежде чем вы не пойдете на запись кода, чтобы решить его. Есть несколько вариаций этой проблемы, но вот версия в этой конкретной задаче:

У вас есть калькулятор, который каким-то образом не работает, чтобы все это левое функциональное, это экран (который может отображать номера от 1 до 1 миллиарда) и двумя другими кнопками. Как ни странно, эти кнопки два, которые не появляются на любом калькуляторе, который я видел, а именно Двойной и а уменьшение (к одной) операция.

Задача состоит в том, чтобы получить от исходного числа на калькуляторе до заданного второго числа, используя только те две функции.

Приведенный пример:

  • Вход: х , y
  • Выход: 2.
  • Объяснение: Используйте двойную работу, а затем операцию уменьшения [2 -> 4 -> 3]

В любом случае, калькулятор вагисты в сторону, код, необходимый для решения этой головоломки, должен:

  • Учитывая любой исходный номер Икс
  • Используя только Двойной (х * 2) и/или уменьшение (х - 1) операции
  • Определите минимальные операции, необходимые для достижения произвольно выбранного у

Сразу очевидно, и (насколько я могу сказать) неправильный способ посмотреть на эту проблему – делать то, что заявлено, и попытаться получить от х к y Отказ Сделав несколько проблем кодирования раньше, я узнал, что часто полезно начать с работы назад от решения, и это также оказалось полезным в этом случае.

Однажды я добрался до этого мышления, остальные встали на место довольно быстро:

  • Если y меньше чем х :

    • Уменьшение х пока это равно y и вернуть количество операций. В этом случае нет удваивания.
    • Более быстрый способ посмотреть это, чтобы просто вернуться X - Y.
  • Если y равен х тогда верните 0
  • Если y больше чем х :

    • Если y нечетное число, приращение y К 1 (поскольку мы идем назад, увеличивая здесь эквивалентное уменьшением, если начать с X )
    • Если y это четное число:
    • Если y больше чем х разделить y К 2 (эквивалентно удваиванию х одна из двух разрешенных операций).
    • Если y меньше чем х Затем увеличить y пока вы не приедете на Икс
  • Петля через эту последовательность до y равно Икс

Это заканчивается, похоже на некоторое изменение следующего кода Python:

# first pass algorithm
def min_ops(x: int, y: int) -> int:
    if y < x:
        return x - y
    ops = 0
    while y != x:
        if y > x and y % 2 == 0:
            y /= 2
        else:
            y += 1
        ops += 1
    return ops

# testing code
if __name__ == '__main__':
    test_cases = [(2, 3, 2)]

    for case in test_cases:
        x, y, expected = case
        result = min_ops(x, y)
        if result != expected:
            print(f"{x}->{y} should have been {expected}, but got {result}")
        else:
            print(f"Correctly solved {x}->{y} in {result} operations")

В этом алгоритме этого алгоритма нет ничего плохого. Это дает правильный ответ, он соответствует критериям лецкода для использования памяти и времени выполнения, и это довольно легко понять.

Однако … Это действительно позволило мне, что мне пришлось итерации через каждое состояние ответа на прибытие в результате. На самом деле, действительно большие числа, это займет много времени. Я знаю я знаю…

  • Это не имеет значения
  • Ответ правильный
  • Это соответствует спецификации
  • Идеальный враг достаточно хорошего
  • Преждевременная оптимизация является корнем всего зла

… и все это.

В реальной жизни я почти наверняка остановился бы здесь, если не было бизнес-причина для оптимизации дальше. Но это были выходные, и хотя погода была идеальной для расходов на улицу, и у меня была тонна других боковых проектов, которые я хотел добраться до того, что он не мог занять слишком дольше, чтобы прибыть в решение, которое мне на самом деле понравилось, верно? … Правильно??

Много часов и 18 листов скретч бумаги позже …

Вот так.

Давайте начнем с случая, где х это 1, и посмотрите, что нужно, чтобы добраться до разных y ценности:

Несколько вещей определенно собираются здесь:

  • Существует четный/нечетной компонент, поскольку последняя операция для любого нечетного числа должна быть уменьшением, а последняя операция для любого четка не может быть уменьшением.
  • Есть «силы 2» элемента. Обратите внимание, что (с x , по крайней мере) набор y = [2, 4, 8, 16, ... 2 ^ n] Составлен исключительно из двойных операций без уменьшения.
  • В дополнение к вышесказанному, можно также увидеть, что существует одинаковое количество двойных операций для всех чисел в комплекте y = [2 ^ n + 1 ... 2 ^ (n + 1)] . Например, каждый из y = [9, 10 ... 15, 16] Имеет ровно 4 двойных операциях, с различным количеством операций по снижению. То же самое верно (но с ровно 3 двойных операций) для y = [5, 6, 7, 8] .
  • Сравнение y = [9, 10 ... 15, 16] Установите еще раз, кажется, что «верхняя» половина является повторением «нижней половины», но вместо каждой строки, имеющей как минимум 3 двойных операции, каждый из которых имеет вместо двух двойных, уменьшение, а затем другой двойной. Наряду с этим «правая сторона» картины от y = [13, 14, 15, 16] повторяется, но одна операция “Правда”.
  • Наконец, один и тот же шаблон на заднем краю y = [9 ... 16] Набор также виден в y = [5 ... 8] установить и выглядит так, будто это может быть начнется в y = [3, 4] установить, но не завершено.

Так рад, что вы спросили …

Давайте начнем с тем же условия для обработки случая, когда y меньше чем х Отказ В этой порции не так много веселья, поскольку это просто вопрос о снижении, пока вы не приедете на результат.

def min_ops_magic(x: int, y: int) -> int:
    if y < x:
        return x - y

Далее давайте воспользуемся тем фактом, что все значения в y = [2 ^ n + 1 ... 2 ^ (n + 1)] иметь одинаковое количество двойных операций. Это количество двойных операций такое же, как мощность 2 для верхней границы этой группы или N + 1 в уравнении выше.

В нашем примере набор y = [9, 10, ... 15, 16] Содержит все номера, такие что Y> X * 2 ^ 3 и y ≤ x * 2 ^ 4 Отказ Поэтому я ссылаюсь на эти номера как в группе « N ». Если я использую эти 4 операции, я остался с этим:

Так что мне нужно определить:

  • Данные входы х и у , что такое самая маленькая n Такое, что y ≤ x * 2 ^ Н.

Есть несколько разных способов кодировать это. Я также расчём 2 ^ n В то же время здесь, так как нам понадобится это позже

Опция 1

import math
from typing import Tuple

def next_pow2(x: int, y: int) -> Tuple[int, int]:
    n = math.ceil(math.log2(y/x))
    pow_of_two = 2 ** n

    return n, pow_of_two

Эта опция работает, потому что:

y * 2 ^ n

y/x ^ n

Журнал (y/x) (2 ^ n)

Журнал (Y/X) * Журнал (2)

Журнал (y/x)/

log2 (y/x)

Округление n До следующего самых высоких целых означает, что мы можем решить для n где угодно в данном диапазоне.

Вариант 2.

from typing import Tuple

def next_pow2(x: int, y: int) -> Tuple[int, int]:
    n = 0
    pow_of_two = 1
    while x * pow_of_two < y:
        pow_of_two <<= 1
        n += 1

    return n, pow_of_two

Эта опция использует битовое смещение для подсчета того, сколько раз нам нужно сместить немного левый (начиная с двоичного 1 ), что та же операция, что и увеличение n :

0001

0010

0100

1000

Первоначально я думал, что вариант 2 был бы быстрее, но после некоторых проверок сроков, похоже, я ошибался, и log2 () Операция была заметно быстрее.

Вариант 3.

from typing import Tuple

def next_pow2(x: int, y: int) -> Tuple[int, int]:
    n = int((y - 1) / x).bit_length()
    pow_of_two = 2 ** n

Эта опция похожа на опцию 2, но он использует встроенный метод Python в целочисленном типе для возврата того, сколько битов необходимо для представления номера. Верхний предел (16, здесь) имеет еще один, чем все остальные числа в группе, поэтому я вычитаю это первым.

Вариант 3 был тот, который я обнаружил в прошлом, и является самым быстрым из трех. Если у вас будет быстрее, чтобы рассчитать этот номер в Python, я хотел бы услышать это!

Добавим опцию 3 на наш алгоритм. Я встроюсь в это, так как мне не нужно звонить из-за того места:

def min_ops_magic(x: int, y: int) -> int:
    if y < x:
        return x - y

    n = int((y - 1) / x).bit_length()
    pow_of_two = 2 ** n

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

Если вы посмотрите на предыдущую диаграмму, вы увидите, что сгруппировал оставшиеся операции определенным образом. Существует J-тетромино (повернутая на 180 °), которое повторяется как в верхней, так и нижней половине группы, а I-тетромино, которое происходит только на верхней половине и толкает верхний j. И нет, тот факт, что в этой головоломке есть тетрис, не странная часть.

Основываясь на работе из предыдущего раздела, я думал, что имел смысл использовать строки, подсчитывая вверх и начиная с верхней границы. Вероятно, не ясно, что я имею в виду, поэтому вот еще одна диаграмма.

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

Смотрите шаблон? Я тоже не был сначала. Попробуй сейчас:

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

Вместо добавления всех битов «1» вместе я только хочу добавить n наименее значимые биты. Я знаю, я знаю … в этом случае это означает, что все биты. Но я обещаю, что это не так для всех х с а также y s.

Давайте получим еще несколько кода. Это только дополнительные две строки, поэтому я просто добавлю его в алгоритм, который мы уже имеем и выделите изменения ниже:

def min_ops_magic(x: int, y: int) -> int:
    if y < x:
        return x - y

    n = int((y - 1) / x).bit_length()
    pow_of_two = 2 ** n

    # count down from top of 'bin'
    idx = x * pow_of_two - y

    # sum the '1's of the `n` least significant bits
    lsb_ones = bin(idx)[-n:].count("1")

Получение индекса довольно прост: просто вычитайте Y от наибольшего числа в этом «bin».

Подводя итоги «1» в n Наименее значимые биты – это симпатичный Python, и есть несколько других способов сделать это также. В Python 3.10 (который должен быть выпущен в этом году), на самом деле есть новый int.bit_count () Метод, который сделает именно это, и намного быстрее. Во всяком случае, эта линия просто преобразует целое число IDX на бинарное строковое представление, нарезать только последний n символы этой строки, а затем подсчитывая, сколько из этих символов "1" Отказ

Опять обратите внимание, что даже хоть В этом конкретном случае Количество битов в этих числах происходит на 3 или меньше (для n ), это не всегда в этом случае, поэтому мы не можем подсчитать все биты.

Мы очень близко сейчас. Как и пообещал, вот случай, когда нам нужно добавить один последний термин, чтобы получить наши общие операции:

Я выбрал несколько, но не совсем случайных х Значение и набор y Значения для этого примера. Не стесняйтесь убедиться, что это работает для других ценностей.

Осторожно, спойлеры: оно делает

Давайте проверимся с х и ведро y = [21, 22 ... 39, 40]

Опять же, я заблокировал n = 2 Операции, и уехали с несколькими тетронимосами, а некоторые большие коробки, которые были бы кошмаром Tetris. Вы можете видеть, что я также заполнил IDX Значения для каждой строки и написания их в двоичном.

Помните, как я сказал, мы можем использовать только n Наименее значительные биты в предыдущем расчете? Посмотрите в этом примере, что произойдет, если бы мы использовали их все. Неправильные ответы повсюду …

Хорошо, так что если мы можем использовать только n LSBS Для этого расчета, почему я все равно записал все MSB?

Только потому, что это последний кусок головоломки.

Для LSBS мы просто суммировали количество битов «1», чтобы получить наши j-тетронимос (тетронима? Idk …) Однако для MSB мы собираемся лечить их как реальные числа, но только после того, как мы получили из-за этих уже использованных LSB.

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

Все, что осталось, это немного кодирования:

def min_ops_magic(x: int, y: int) -> int:
    if y < x:
        return x - y

    n = int((y - 1) / x).bit_length()
    pow_of_two = 2 ** n

    # count down from top of 'bin'
    idx = x * pow_of_two - y

    # sum the '1's of the `n` least significant bits
    lsb_ones = bin(idx)[-n:].count("1")

    # bit-shift `n` to the right to get only the MSBs
    msb_vals = idx >> n

    # the grand finale
    return n + lsb_ones + msb_vals

Так как мы уже получили то, что нам нужно от n LSBS индекса, просто сдвиньте его прямо по n биты, чтобы попасть на MSB, которые мы заботимся о. Специальное преобразование не требуется после этого – мы все еще просто рассматриваем его как целое число.

Наконец, ответ на вопрос жизни, вселенной и все (или, по крайней мере, это головоломка летета) – это просто сумма 2-х годов, сумма n Значения LSB «1» и число, представленное оставшимися MSBS.

Сумасшедший, верно?

TLDR; да

Помимо всего, что я узнал из этого (в том числе много чистки на битовой математике, что я в конечном итоге не нуждался в решении для решения), я также оказался более удовлетворительным (мне), быстрее (для большинства случаев) решение. Ваши результаты могут варьироваться, но на моей машине для произвольного набора ~ 80 различных условий ввода, «магическое» решение на ~ 20% быстрее.

Бонусные баллы, если вы можете посмотреть, как это объяснение относится к Hamming вес и/или к этому не связанным Кодирование вызов (Извините, я не могу найти ссылку на английском языке, но Google Translate работает достаточно хорошо).

Как всегда, пожалуйста, дайте мне знать, если вы найдете какие-либо ошибки в коде или объяснении, или если у вас есть вопросы.

Оригинал: “https://dev.to/ansonvandoren/solving-the-broken-calculator-problem-the-hard-way-4a1m”