Решение любопытной цепочки задач c leetcode или сеанс древней алгоритмической магии с последующим разоблачением

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Некоторое время назад я наткнулся на забавную цепочку задач на сайте leetcode.com Сами задачки не слишком сложны, но их решения довольно любопытны. Кроме того, задачки такого типа довольно часто попадаются на собеседованиях в крупных компаниях и понимание методов их решения может быть довольно полезно.


Первая задачка — 136. Single Number (легкая сложность)

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

Нужно решить задачу за O(n) и c константной по дополнительной памятью.


Пример 1

Вход: nums = [1, 3, 3, 2, 6, 2, 1]

Результат: 6

Пример 2

Вход: nums = [12, 1, 1, 7, 1, 12, 1]

Результат: 7

Пример 3

Вход: nums = [6]

Результат: 6

Посмотреть решение

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

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

Вот короткий код решения на python3:

def single_number_i(nums: list) -> int:

    result = 0

    for el in nums:

        result ^= el

    return result

Мы используем всего лишь столько дополнительной памяти, сколько занимает int и находим решение за единственный проход по заданному массиву, что дает нам O(n) по сложности. Это чистое и красивое решение.


Вторая задачка - 137. Single Number II (средняя сложность)

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

Нужно решить задачу за O(n) и c константной дополнительной памятью.


Пример 1

Вход: nums = [3, 1, 3, 3]

Результат: 1

Пример 2

Вход: nums = [12, 1, 1, 5, 1, 12, 12]

Результат: 5

Пример 3

Вход: nums = [6]

Результат: 6

Посмотреть решение

К сожалению, мы не можем воспользоваться предыдущим трюком, так как в данном случае мы не сможем превратить парные биты в нужной позиции в нули. Было бы заманчиво привести данный нам массив к виду из предыдущей задачи, а затем решить его аналогичным способом. Рассуждая таким образом, можно легко заметить, что если бы мы знали, что пробегая по массиву мы уже встречали число N дважды(ну или трижды), то мы могли бы добавить к получаемой нами сумме дополнительный XOR с N, сделав итоговое число XOR c этим числом четным, таким образом убрав его из итоговой суммы, а то что останется и будет нашим ответом.

def single_number_ii(nums: list) -> int:

    mem = {}

    result = 0

    for el in nums:

        if not mem.get(el, False):

            mem[el] = 1

        else:

            mem[el] += 1

            if mem[el] == 2:

                result ^= el

        result ^= el

    return result

К сожалению, максимально это решение потребует (len(nums)-1)/3 по памяти, что нельзя назвать константным потреблением, так что придется нам искать другое решение.

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


def single_number_137_ii(nums: list) -> int:

    ans, acc = 0, 0

    for n in nums:

        ans = ans ^ n & ~acc

        acc = acc ^ n & ~ans

    return ans

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


Третья задачка - 260. Single Number III (средняя сложность)

Дан непустой массив целых чисел, все элементы которого, встречаются по два раза и два элемента в массиве встречаются единожды. Мы должны найти эти уникальные элементы, за O(n) и c константной дополнительной памятью, причем их порядок неважен.

Пример 1

Вход: nums = [1, 2, 1, 3, 2, 5]

Результат: [3, 5]

Пример 2

Вход: nums = [1, -2]

Результат: [-2, 1]

Посмотреть решение

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

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

def single_number_260(nums: list) -> int:

    res1, res2 = 0, 0

    glob_xor = 0

    for n in nums:

        glob_xor ^= n

    diff_bit = glob_xor & ~(glob_xor + 1)

    for n in nums:

        if n & diff_bit:

            res1 ^= n

        else:

            res2 ^= n

    return [res1, res2]


Несмотря на то, что нам пришлось пройти массив 2 раза, это по прежнему O(n) по сложности и всего 2 int’a по памяти.


Примечание 1: Несмотря на то, что int в Python это совсем не то же самое, что int в других языках, тем не менее примем его размер за константу.

Примечание 2: Если вам понравились подобные решения, то могу порекомендовать прекрасную книгу Henry S. Warren, Jr. “Hacker’s Delight”, где описано множество полезных трюков битовой арифметики (в русском переводе, она называется “Алгоритмические трюки для программистов”).

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Интересно ли читать подобные статьи на хабре?
40% Да, хочу больше таких публикаций 2
60% Нет, спасибо, мы тут не для разборов алгоритмов собрались 3
Проголосовали 5 пользователей. Воздержавшихся нет.
Источник: https://habr.com/ru/articles/764718/


Интересные статьи

Интересные статьи

В этом году наша компания впервые провела конкурс по базам данных в рамках международной олимпиады IT-Планета по информационным технологиям. Раньше на олимпиаде использовалась СУБД Oracle; наш коллега...
В этой статье я углублённо сравню потребление памяти между асинхронными и многопоточными программами популярных языков вроде Rust, Go, Java, C#, Python, Node.js и Elixir. Недавно я проводил сравн...
Видеоблогер Конор Хекстра использовал разные языки программирования, чтобы решить одну и ту же задачу. Попутно выяснилось, что у Фортрана полно поклонников.
Разберемся на простейших примерах, когда уместно использовать логарифмическое преобразование целевой переменной в задаче линейной регрессии, а когда можно этого не делать.
Когда сенолитики вызывают гибель сенесцентных клеток, иные (более юные) клетки обязаны реплицироваться и занимать место убитых клеток. Эта репликация клеток вызывает укорочение теломер, что ...