Делать вещи простыми и эффективными.
СОДЕРЖАНИЕ
∘ Задача
∘ Понимание задачи
∘ Пошаговое выполнение
∘ Набор основных математических операций
∘ Использованные контрольные примеры
∘ Ссылки
Мне очень нравятся испытания Codility. Это прекрасный (и совершенно бесплатный) способ улучшить свои навыки решения проблем и знание языков программирования. Они также являются отличным способом «разогреть» ваш мозг для технических собеседований, тем более что большинство ваших решений оценивается по их способности справляться с крайними случаями, оставаясь при этом эффективными по времени.
Большая часть современного профессионального развития состоит из конфигурирования и объединения библиотек вместе, поэтому приятно погрузиться в задачу, которая является чисто логической и ориентированной на производительность.
За последние пару лет я медленно работал над их уроками и задачами по выходным и праздникам, и это помогло мне сохранить настрой на поиск решений «жадного алгоритма» и рассмотрение как производительности, так и удобочитаемости.
Один конкретный урок застал меня врасплох. Задачи доброжелательности оцениваются в порядке сложности: «Безболезненно», «Уважительно» или «Амбициозно». Эта задача получила оценку «респектабельная», но ее можно эффективно решить с помощью одной строчки кода.
Задание
Уроки Codility состоят из материала для чтения в формате PDF и набора «задач». Первое задание урока «Суммы префиксов» называется «Подсчет Div». Инструкции следующие:
Напишите функцию ... которая для трех целых чисел A, B и K возвращает количество целых чисел в диапазоне [A..B], которые делятся на K
Гарантируется, что A ≤ B.
Понимание проблемы
Чтобы получить более полное представление о проблеме, я иногда нахожу полезным набросать ее, совершенно не беспокоясь об эффективности.
Очевидное решение методом перебора - перебрать все целые числа между A и B, проверяя, делятся ли они на K, используя по модулю (%).
Чтобы быть в безопасности, нам также нужно проверить, что B! = 0:
def solution(a, b, k): if b == 0: return 0 count = 0 for ii in range(a, b + 1): if ii % k == 0: count += 1 return count
Или, что гораздо более кратко, используя тернарный оператор в понимании списка, чтобы дать нам количество (len
) всех чисел, которые соответствуют нашим критериям:
def solution(a, b, k): return 0 if b == 0 else len([ii for ii in range(a, b + 1) if ii % k == 0])
Это будет выполняться за линейное время - O(n)
- что неидеально, если A равно 0, а B - возможное максимальное значение теста в 2 миллиарда.
Итерация по шагам
Точно так же, как в приведенном ниже примере, поиск наименьшего и наибольшего кратного k в диапазоне от A до B и повторение с шагом, равным K, все равно оставит нас с O(n)
, что особенно неэффективно, если K равно 1.
def solution(a, b, k): if b == 0: return 0 first_divisible = a if a % k == 0 else a + (k - a % k) last_divisible = b - b % k return len(range(first_divisible, last_divisible + 1, k))
Но здесь вступает в игру некоторая магия Python: язык эффективно знает len
диапазона, даже со значением шага. Это работает.
Но это также показывает более удобочитаемое, краткое и эффективное решение. Теперь мы можем видеть, что все, что нам нужно увидеть, это то, сколько раз кратное K встречается между A и B.
Набор основных математических операций
Давайте использовать исходный пример из Codility, a = 6, b = 11, k = 2.
11 / 2 = 5.5
- которое мы можем округлить до 5, чтобы получить общее количество способов, которыми 2 делится на 11(6 - 1) / 2 = 2.5
- которое мы можем округлить до 2 для количества способов, в которых целые числа меньше 6 без остатка делятся на 2. Мы можем вычесть их из суммы, указанной выше.5 - 2 = 3
- вычтите исключенное количество из общего количества, чтобы получить наш результат.
В операторе деления этажа (//
) частное автоматически округляется в меньшую сторону, возвращая значение с плавающей запятой, которое мы можем преобразовать в целое число для согласованности. Это делает наше решение аккуратным и эффективным, работающим за O(1)
постоянное время.
def solution(a, b, k):
return 0 if b == 0 else int(b // k - (a - 1) // k)
Нам по-прежнему нужно обрабатывать крайний случай, когда B равно нулю, но снова тернарный оператор делает код коротким и легким для чтения.
Любой из этих двух последних примеров все равно даст 100% результат на Codility.
Пожалуйста, дайте мне знать, если что-то здесь не имеет смысла или вы можете придумать еще более краткий способ достижения этого результата.
Используемые тестовые примеры
Я подумал, что было бы полезно включить тестовые примеры, с которыми я работал при создании этих примеров.
cases = [ [4, 10, 3, 2], [10, 10, 5, 1], [10, 10, 7, 0], [10, 10, 20, 0], [0, 0, 10, 0], [5, 100, 6, 16], [5, 100, 5, 20], [5, 102, 6, 17], [0, 2, 2, 2], [0, 5, 5, 2], [0, 5, 1, 6], [1, 5, 1, 5], [0, 2000000000, 1, 2000000001], [0, 2000000000, 2, 1000000001], ]
Они были подготовлены с использованием синтаксиса: [a, b, k, expected], чтобы их можно было включить в тест цикла, например:
def test_solution(self): for case in self.cases: with self.subTest(case): self.assertEqual(solution(case[0], case[1], case[2]), case[3])
использованная литература
Урок Codility Prefix Sums
Подготовьтесь к техническим собеседованиям и развивайте свои навыки программирования с помощью наших практических уроков программирования. Станьте сильным техническим специалистом… app.codility.com »
Примечание: эта статья ранее была опубликована с примерами кода на Ruby.
Больше контента на plainenglish.io