Объяснение студента, изучающего информатику.
Как случайный программист, я часто проверяю такие сайты, как Codeforces и Kattis, чтобы отточить свою логику программирования и навыки. Однажды я наткнулся на интересную для меня проблему. Сначала эта проблема выглядела довольно сложной, поэтому я попытался ее решить (проблема будет объяснена в следующих разделах). Сначала я не мог решить ее - я не мог найти никаких закономерностей, связанных с проблемой. Я никуда не торопился, поэтому оставил задачу, потому что уже полночь.
Однажды, когда я ничего не делал в торговом центре, я вернулся к проблеме и понял, как это сделать - с помощью манипуляции с битами!
Что такое битовые маски?
Поскольку я все еще учусь, я все еще узнаю гораздо больше о компьютерных науках, поэтому пока это то, что я знаю о манипуляциях с битами.
Чтобы понять манипуляции с битами, нам нужно сначала понять битовые маски. Вы спросите, что такое битовые маски? Как вы, возможно, уже знаете, немного не хватает «двоичных цифр» - цифры, состоящей только из нулей и единиц. Целочисленная переменная обычно имеет ограничение в 32 бита, что означает, что она состоит из 32 бита с диапазоном 2 ² (2 выводятся из состояния битов - 0 или 1, что соответствует двум возможностям).
С другой стороны, битовые маски - это просто целые числа, используемые как набор логических значений. Поскольку логические значения имеют состояние только True или False, они также могут быть представлены нулями и единицами, и именно поэтому они называются битовыми масками! Итак, просто поставьте:
Битовые маски используют то, как целое число может быть представлено в его двоичной форме, что означает, что оно может использоваться как компактный набор логических значений.
Побитовые операторы.
Битовые манипуляции - это техника, которая используется для манипулирования битами, представляющими целое число. Я собираюсь показать вам некоторые часто используемые побитовые операторы в C ++.
- Сдвиг влево и вправо (‹
Сдвигает двоичную форму целого числа влево / вправо в зависимости от количества сдвигов.
Обратите внимание, что сдвиг влево эквивалентен умножению на 2, а сдвиг вправо - это деление на 2 и округление - обычно это быстрее, чем обычные операции деления.
int x=5; // 101 in binary If shifted left by 1 : x=x<<1; //10 = 2 in decimal If shifted left by 3: x=x<<3; //0 = 0 (see it as 0101) If shifted right by 1 : x=x>>1; //1010 = 10 in decimal If shifted right by 2: x=x>>2; //10100 = 20 in decimal
2. Побитовое ИЛИ (|)
Выполняет операцию ИЛИ над двоичной формой двух целых чисел.
int x=5; // 101 in binary int y=9; // 1001 in binary int z=x|y; x = 0101 y = 1001 --------- OR z = 1101 = 13 in decimal
3. Побитовое И (&)
Выполняет операцию И над двоичной формой двух целых чисел.
int x=5; // 101 in binary int y=9; // 1001 in binary int z=x&y; x = 0101 y = 1001 --------- AND z = 0001 = 1 in decimal
4. Побитовое НЕ (~)
Инвертирует биты двоичной формы целого числа.
Эта операция обычно используется вместе с другими операторами; например, для выполнения операций NAND или NOR.
5. Побитовое исключающее ИЛИ (^)
Применяет операцию XOR к двоичной форме двух целых чисел.
int x=5; // 101 in binary int y=9; // 1001 in binary int z=x^y; x = 0101 y = 1001 --------- XOR z = 1100 = 12 in decimal
Интересный факт: поменять местами 2 целых числа можно с помощью операции XOR; попробуйте сами!
int a=3; int b=5; a=a^b; // a=6, b=5 b=a^b; // a=6, b=3 a=a^b; // a=5, b=6
Помните, что это только битовые операторы, их использование будет показано в примере. Редко использовать его в чистом виде, как в примерах - мы можем сделать гораздо больше!
Проблема с использованием Bitmask!
Возвращаясь к моей истории решения этой проблемы в Kattis, вот проблема, о которой я говорил в предыдущем разделе.
Краткое изложение проблемы: бесконечное двоичное дерево, каждый узел которого состоит из дроби с числителем p и знаменателем q . Левый узел равен p / (p + q), , а правый узел равен (p + q) / q. Функция F (n) определена так, что она будет возвращать дробь из соответствующего узла. Узлы пронумерованы, как показано на иллюстрации, взятой из задачи ниже:
Первый узел начинается со значения p = 1 и q = 1. Это означает, что F (1) = 1/1, F (2) = 1/2, F (3) = 2/1 и т. Д. И т. Д. Нам необходимо ввести значение дроби и вывести N. Будет несколько тестовых примеров.
На первый взгляд, эта проблема может не иметь ничего общего с манипуляциями с битами. Но на самом деле он имеет два состояния - переход к левому или правому узлу! Прежде чем я изложу вам свою точку зрения на эту проблему, вы можете попробовать решить ее самостоятельно.
Мое понимание этой проблемы.
Проблема может предложить вам вручную использовать метод поиска в ширину (BFS) с использованием очереди. Для дроби, которая находится в малом N, это было бы жизнеспособным. Однако мы не знаем, где будет находиться введенная дробь, поскольку это бесконечное двоичное дерево.
Итак, как решить проблему? Вот моя логика:
- Лучше искать прямо из каждого теста, чем обрезать, так как это займет слишком много времени и памяти.
- Мы можем представить каждый узел в его двоичной форме!
- p всегда соответствует ‹q в левом узле, а p всегда составляет› q в правом узле.
Что вы имеете в виду, используя его двоичную форму?
Что ж, давайте возьмем первый, второй и третий узел в их двоичной форме (1, 10, 11). С помощью простого анализа мы можем сначала предположить, что в левом узле мы добавляем 0 из первого узла, а в правом узле мы добавляем 1 из первого узла.
Мы можем дополнительно проверить правильность нашего анализа, посмотрев на паттерн; и это оказывается правильным! (См. Рисунок для лучшего визуального объяснения.)
Почему это работает?
Просто - это бинарное дерево! Ну, конечно, N будет следовать образцу двоичных цифр!
Поскольку мы решаем задачу сверху вниз, нам нужно добавлять цифры (если слева 0, если справа 1) спереди. Как добиться такого результата? Мы используем оператор ИЛИ с оператором сдвига влево, чтобы включить бит в указанном месте.
Взгляните на этот псевдокод:
int result=0; int pos=2; result|=(1<<pos); //turning on the 2nd bit from the right (0-based)
Таким образом, мы можем просто поставить 1, если узел идет с правой стороны, и каждый уровень, на который мы поднимаемся, мы увеличиваем переменную pos
на единицу! Мы делаем это, пока не дойдем до последнего узла (то есть p = 1 и q = 1).
Вот мое решение проблемы, написанное на C ++:
Обратите внимание: поскольку мы начинаем с индекса одного, мы добавляем еще 1 после достижения начального узла.
Это решение, поскольку мы перемещаемся по двоичному дереву, временная сложность программы составляет O (log n)!
Заключение!
Эта проблема - лишь один из примеров использования битовых манипуляций. Некоторые другие приложения также интересны для изучения, например, обратное отслеживание с использованием битовых масок, динамическое программирование с битовыми масками и другие.
Я надеюсь, что это руководство может стать хорошим началом для понимания методов работы с битами или обогатить читателей новыми знаниями.
Спасибо за чтение!