Еженедельное испытание III

Хотя я провел годы, возясь с крошечными фрагментами JavaScript и PHP во времена моего партнерского маркетинга, мой первый настоящий опыт программирования был около 3 лет назад, когда кто-то, с кем я встречался, познакомил меня с Project Euler.

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

Я быстро пристрастился к этим вызовам; время между как, черт возьми, это вообще возможно? до ура! Я решил это! удивительно коротко. Я проглотил учебник Python, решая такие задачи, как оценка суммы всех дружественных чисел до 10000.

Я очень взволнован тем, что возвращаюсь к своим корням. На этой неделе я буду решать Задачу 92, связанную с счастливыми числами.

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

Счастливое число — это число, цифры которого при возведении в квадрат и суммировании в конечном итоге достигают «1». Вот как это работает:

Проблема Эйлера требует от меня найти, сколько несчастливых чисел меньше 10 миллионов.

Рубин!

За последние 6 месяцев, как лично, так и профессионально, почти весь мой код был на Javascript. Я чувствовал себя как в отпуске. Ruby — удивительно лаконичный язык с красивым синтаксисом.

Попытка №1 — грубая сила

В Project Euler есть много проблем, у которых нет очевидной отправной точки. Это не такая уж проблема.

Подход грубой силы вполне очевиден: перебрать от 1 до 10 000 000, проверяя каждое число по пути на «счастье».

Вот это решение с множеством комментариев:

Хорошая новость: это работает! Он находит правильный ответ (8 581 146 — большинство чисел несчастливые 😞).

Плохая новость: для запуска требуется 278,4 секунды. Это более 4 минут.

Эмпирическое правило Project Euler заключается в том, что ваши решения не должны запускаться более 60 секунд на домашнем ПК средней мощности. Учитывая, что Project Euler существует уже более 10 лет, а средний домашний ПК сильно изменился за это время, я бы хотел, чтобы мое решение выполнялось за ‹20 секунд.

Другими словами, у меня много работы.

Попытка № 2: поиск хэша

Самая большая проблема — по крайней мере, так я думал в то время — заключалась в том, что я проходил через полный цикл для каждого отдельного числа от 1 до 10 миллионов. Что, если бы раньше существовал способ определить, является ли число счастливым или несчастливым?

Я заметил интересную закономерность. Чем больше число начинается с, тем быстрее оно уменьшается с каждой итерацией.

Самая большая цифра от 0 до 9 — это, очевидно, 9. 9² — это всего лишь 81. Это означает, что число «999» разрешается в 81*3, или 243. Число «99999», которое на несколько порядков больше, разрешается в 81*5 или просто 405.

Самое большое число, которое нас интересует, — 9 999 999. Это разрешается до 567, и оттуда становится только меньше. Это означает, что всего после одной итерации самое большое число, о котором нам когда-либо приходилось беспокоиться, равно 567.

Что, если мы начнем с итерации от 1 до 567 и сохраним, является ли число счастливым или нет, в хеше? Затем, при итерации до 10 миллионов, мы можем выполнить 1 итерацию вычисления, а затем посмотреть его в хэше, чтобы увидеть, счастлив он или нет?

Вот как выглядит это решение:

Итак, это определенно лучше, но решение все еще занимает слишком много времени: 67,0 секунды.

Затем я начал думать, что, может быть, массивы будут быстрее, чем хэши? Недавно я узнал, что хеш-алгоритмы удивительно сложны — может быть, использование массивных индексов было бы более эффективным?

Новый синтаксис будет просто массивом логических значений: [false, true, true] вместо {1: false, 2: true, 3: true}. Я не буду размещать код здесь, так как он очень и очень похож на версию 2, но любопытные глаза могут увидеть его на GitHub.

Конечный результат был маргинален. 63 секунды вместо 67. Оказывается, алгоритмы хеширования довольно эффективны!

Окончательная версия: Джош, ты идиот!

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

Чтобы выполнить одну итерацию моего метода квадратов и сумм, я:

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

Это слишком много конверсий. И это совершенно ненужно. Та красивая лаконичная линия Руби, которой я был так доволен? Это меласса.

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

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

Например, он может превратить любой массив в круговой массив.

Это окончательное решение выполняется на моем компьютере всего за 6 секунд. Это сокращение более чем на 90%.

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

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

Как всегда, все 4 решения можно найти на GitHub.

Если вы дошли до этого места, большое спасибо за чтение =) Есть ли у вас какие-либо предложения по проблеме, которую я должен решить на следующей неделе? Я хотел бы услышать от вас!

-Джош