Практическое руководство по сложной теме
Вам нужно знать Bit Manipulation и Bitmask для собеседования по программированию?
Эти две темы считаются продвинутыми материалами и «неявно» не требуются для собеседования по кодированию. Тем не менее интервьюер имеет полное право задать вам эти вопросы, если захочет.
Это краткий обзор всех стандартных методов и шаблонов, которые вы должны знать для решения проблем с битовыми масками.
Оглавление
· Основные
∘ Общие операции для битовых манипуляций
∘ Важные операции для битовых масок
· Шаблоны битовых манипуляций
∘ Подсчитайте количество 1 бит в двоичное представление числа
∘ Проверить, является ли положительное число степенью двойки
∘ Эффективно вычислить Pow(x, n), где x › 0 и n › 0
· Шаблон битовой маски
∘ Общий шаблон:
∘ По заданному массиву уникальных элементов найти все возможные подмножества
∘ Посчитать все возможные подмножества размера k, сумма которых к заданной цели
· Вывод
Все коды и уравнения написаны автором.
Базовый
Битовая маска — это идея использования двоичного представления чисел для решения сложных проблем. Это представление уникально для каждого числа.
Пример. Количество битов числа 11 равно "1011", потому что в системе счисления 2:
11 = 8 + 2 + 1 = 1*2³ + 0*2² + 1*2¹ + 1*2⁰= “1011”
При работе с битовой маской: Неважно, каково фактическое число. Все, о чем мы должны заботиться, это его двоичная форма и следующие операции:
Общие операции для битовых манипуляций
- Сдвиг влево: чтобы сдвинуть
x
влево на n пробелов, мы используемx << n
. Например, в двоичной форме"1011" << 3 = "1011000"
или11 << 3 = 88
. - Правый Shift: аналогично используем
x >> n
. Например,"10101" >> 3= "101"
(обратите внимание, что единичные биты исчезли). - Очистить младший установленный бит:
x & (x — 1)
(например,"10100" -> "10000"
) - И:
a & b = 1
, еслиa = 1
Иb = 1
, иначе 0 - ИЛИ:
a | b = 1
, еслиa = 1
ИЛИb = 1
, иначе 0 - XOR:
a ^ b = 1
, если ровно одно изa
илиb
равно 1, иначе 0 - Для чисел больше 1 мы применяем эти операции итеративно к каждой битовой позиции. Например.
"10111" AND "1100" = "00100" = "100"
Важные операции с битовыми масками
- Превратите i^й бит в 1:
mask = mask | (1 << i)
илиmask |= (1 << i)
- Перевернуть i^й бит:
mask = mask ^ (1 << i)
илиmask ^= (1 << i)
Большую часть времени вы будете манипулировать числом под названием mask
. Эта маска будет использоваться для отслеживания «используемых» или «неиспользуемых» элементов в списке.
Пример. Учитывая набор из 3 элементов A = {1,2,3}
, как мы можем эффективно представить все подмножества этого набора?
Ответ:Используйте числа от 0 до 7 и соответствующие им битовые маски, где каждая 1-битная позиция сигнализирует выбранному элементу:
0 = "000" = {} = the empty set 1 = "001" = {1} 2 = "010" = {2} 3 = "011" = {1, 2} 4 = "100" = {3} 5 = "101" = {1, 3} 6 = "110" = {2, 3} 7 = "111" = {1, 2, 3}
В общем случае, имея набор из n
элементов, мы всегда можем представить все подмножества этого набора с помощью чисел от 0 до 2^n — 1
.
И это основная идея битовой маски. Мы будем манипулировать mask
, представляющим текущее подмножество данного списка.
Шаблоны битовых манипуляций
Подсчитайте количество 1 бит в двоичном представлении числа
Мы можем продолжать сдвигать число вправо, пока оно не станет равным нулю. На каждом шаге мы проверяем, равен ли 0^й бит единице, и добавляем его к общему счету:
Мы можем дополнительно оптимизировать это решение: вместо того, чтобы каждый раз сдвигать на 1 бит, мы можем удалить самый младший установленный бит и сдвинуть соответственно:
Проверить, является ли положительное число степенью двойки
Если n является степенью двойки, то для некоторого положительного целого числа k
Другими словами, двоичное представление n имеет вид "100...0"
. Затем мы можем легко проверить это, удалив младший битовый набор n:
Эффективно вычислить Pow(x, n), где x > 0 и n > 0
Рассмотрим двоичное представление n на примере 11:
Обратите внимание, что каждая позиция в двоичном представлении числа 11 соответствует показателю степени x, x², x⁴, …
Таким образом, вместо того, чтобы зацикливаться и умножать n раз для вычисления ответа, мы можем оптимизировать вычисления, сохраняя только степени двойки: x, x², x⁴, x⁸,… и перемножая соответствующие значения, чтобы получить наш окончательный ответ:
Шаблон битовой маски
Напомним, что маска представляет собой подмножество.
Например, у меня есть маска с надписью "00110011"
.
Что это за число в десятичной форме?
Я не знаю, и мне все равно.
Все, что нам нужно знать, это то, что эта маска представляет собой состояние множества. Эта конкретная маска означает, что мы обрабатываем подмножество элементов с индексами 0, 1, 4 и 5 (идем справа налево).
Общий шаблон:
Что делает этот кусок кода?
Мы перебираем все индексы в наборе. Если мы встретим «неиспользуемый» элемент по индексу i
, то соответствующий элемент маски будет "0"
. Таким образом, not mask
по индексу i
равно "1"
, поэтому not mask & (1 << i)
вернет True. Затем мы можем добавить этот элемент в наше подмножество и обработать его дальше.
Учитывая массив уникальных элементов, найдите все возможные подмножества
Напомним из предыдущего раздела: при наличии множества размером n
мы можем использовать числа от 0 до 2^n-1
для представления всех подмножеств.
Для каждого числа мы создаем соответствующее подмножество, просматривая его двоичное представление и находя положение всех "1"
. Затем мы можем добавить подмножество к нашему окончательному результату.
Пример вывода:
>>> A = [1,3,5,7] >>> subset(A) [[], [1], [3], [1, 3], [5], [1, 5], [3, 5], [1, 3, 5], [7], [1, 7], [3, 7], [1, 3, 7], [5, 7], [1, 5, 7], [3, 5, 7], [1, 3, 5, 7]]
Подсчитайте все возможные подмножества размера k, сумма которых соответствует заданной цели.
Эта задача называется Combination Sum III на Leetcode. Мы хотим найти все подмножества размера k
, которые в сумме дают n
, такие что:
- Используются только номера с
1
по9
. - Каждое число используется не более одного раза.
Эту проблему можно решить с помощью обратного отслеживания, но я хочу представить здесь решение с использованием битовой маски. Как только вы освоитесь с битовыми манипуляциями, вы можете обнаружить, что проще решать и/или писать решения, используя битовые маски.
Наша начальная маска равна 0, что представляет собой пустой набор (это будет иметь место в 99% случаев). На каждом шаге мы выбираем включение любого бита в позициях от 1 до 9 (справа налево, с начальной позицией, проиндексированной 0).
Например, "1010101010"
имеет "1"
на позициях 1, 3, 5, 7 и 9. Эта маска представляет набор {1,3,5,7,9} с суммой, равной 25.
Мы можем решить проблему, используя Bitmask и DP. Когда у нас есть набор всех допустимых масок, мы можем декодировать каждую маску и получить окончательный набор чисел.
Вот решение, в котором используются три различных метода манипулирования битами, обсуждаемые в этом посте:
Заключение
Bitmask не должен пугать.
Когда я начал готовиться к собеседованию по программированию, я обнаружил, что битовая маска — самая сложная тема. Сначала я бы вообще пропустил эти вопросы. Однако по мере того, как я решаю все больше и больше проблем, я в конце концов понял, что все не так страшно, как я себе представлял.
Некоторым из моих друзей действительно задавали вопросы о битовой маске во время их интервью (в какой-то Большой Технологии). Уровень сложности аналогичен двум задачам, представленным в разделе «Шаблон битовой маски». Вопросы предполагалось решать с помощью обратного отслеживания. Но решение с помощью битовой маски — отличный способ произвести впечатление на интервьюеров!
(Скорее всего, им это тоже не очень удобно.)
Если вы хотите поддержать четверг, вы можете зарегистрироваться, чтобы стать участником Medium. За 5 долларов в месяц вы получите неограниченный доступ к историям на Medium. Чт также получит небольшую комиссию, если воспользуетесь его реферальной ссылкой.