Блог пользователя vogel

Автор vogel, 8 лет назад, По-русски

Сейчас пишу статью на wiki — конспекты про битовые операции, трюки и прочую битовую магию. Если вы знаете полезные трюки с битами, которые применяются в алгоритмах, а также просто прикольные вещи, то делитесь своими трюками в комментариях. Также приветствуются извращённые решения известных задач. Например: обменять значения переменных, не использую третью переменную с помощью XOR.

Приветствуются комментарии в виде:

  • Описание задачи
  • Решение задачи в виде кода, по-возможности короткое объяснение для не очевидных трюков.

Пример идеального комментария:
Является ли число степенью двойки?

unsigned int v; // we want to see if v is a power of 2
bool f;         // the result goes here 

f = v && !(v & (v - 1))

Короткое объяснение: Пусть v — это степень двойки (в двоичном представлении 1 единица и k нулей справа), тогда v — 1 в двоичном представлении это число, состоящее из k единиц. Очевидно, что побитовое И чисел
v и v — 1 должно давать в результате 0.

Задача для затравки: Найти младший единичный бит за O(1)

UPD: Интересные ссылки из комментов
  • Проголосовать: нравится
  • +50
  • Проголосовать: не нравится

»
8 лет назад, # |
Rev. 5   Проголосовать: нравится +3 Проголосовать: не нравится

Младший единичный бит так вроде?
((a^(a-1))+1)>>1

Вариант покрасивее:
(a^(a-1))&a

»
8 лет назад, # |
Rev. 4   Проголосовать: нравится +184 Проголосовать: не нравится

Является ли число степенью двойки?

Проверка неполна: надо еще проверить, что v ≠ 0. Более того, стоит отдельно указать, что происходит в знаковых переменных (например, иногда — undefined behavior на плюсах).

обменять значения переменных, не использую третью переменную с помощью XOR.

Только не надо, пожалауйста, учить людей undefined behavior без указания того, к чему это может привести (aka писать "в одну клёвую команду").

По делу (если что-то из этого вам непонятно, могу расписать):

  1. Битовые маски — храним подмножества множества из 32/64/128 фиксированных элементов. Каждый бит — есть элемент или нет. Дополнение (~, не !), пересечение (& и &=), объединение (|, |=), установка бита по номеру (| 1 << x), снятие бита по номеру (& ~(1 << x)), сдвиги влево-вправо (стоит сделать замечание про знаковость в плюсах и про >>> в Java).
  2. Подсчёт количества единиц в числе за (где w — разрядность) методом "разделяй и властвуй". Обычно работает медленнее предподсчёта, но надо сравнивать
  3. Перебор всех над/подмножеств множества за O(Ans) одним циклом for. В частности — перебор всех пар "множество/его подмножество" за O(3n).
  4. Дерево Фенвика: какое было изначально определение, как и в какие формулы оно сворачивается
  5. Для GCC: __builtin_popcount, __builtin_ctz, __builtin_clz и прочая, прочая, прочая (в том числе с суффиксами ll).
  6. Кодирование кортежей из небольших чисел (например, 0-3) в битмаске: установить значение i-го элемента, получить и так далее.
  7. Два способа получения значения бита: (a >> x) & 1 и a & (1 << x), чем отличаются. Если в плюсах писать внутри условия if, то ничем. А вот если пытаться немедленно использовать результат операции внутри другой, то возможны варианты: первая штука возвращает 0/1, а вторая — 0/x-ый бит, в разных местах нужно разное.
  8. Битовое сжатие: алгоритм Флоида за и std::bitset
  9. Выражение сложения через O(w) битовых операций: просто на каждом шагу считаем отдельно XOR и перенос. Вроде можно еще за битовых операций (надо научиться быстро считать переносы при помощи этакого дерева отрезков).
  10. Определение знака целого числа взятием старшего бита.
  11. Низкий приоритет у ^ и << в плюсах по сравнению с некоторыми другими операциями (например, сравнениями или сложениями).
  12. Быстрое приближение обратного квадратного корня.

Еще можно загуглить и получить вот это. Пара вещей оттуда (не проверял):

  1. Определить, имеют ли целые числа разные знаки: (x ^ y) < 0.
  2. Подсчёт числа бит в 14-битном значении через умножение на многочлен в кольце : (v * 0x200040008001ULL & 0x111111111111111ULL) % 0xf;
  • »
    »
    8 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    WOW! Очень круто.

  • »
    »
    8 лет назад, # ^ |
    Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

    Спасибо, уже добавил в избранное :).

    если что-то из этого вам непонятно, могу расписать

    Распишите, пожалуйста, 7, 8, 9 и 12 (КАК оно работает o_O?)

    UPD yeputons и vogel, спасибо, теперь понятно!

    • »
      »
      »
      8 лет назад, # ^ |
      Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

      7) Просто тебе нужно узнать в маске a бит под номером x. Ты сдвигаешь единицу на x бит влево и & с маской или можно сдвинуть маску на x бит вправо и & с 1.

      • »
        »
        »
        »
        8 лет назад, # ^ |
          Проголосовать: нравится +5 Проголосовать: не нравится

        Именно. В плюсах, например, в некоторых случаях неважно (в Java аналогично, но надо дописать != 0):

        if ((a >> x) & 1) // is the same as
        if (a & (1 << x)) // this
        

        А вот в некоторых — важно, какой бит мы получаем:

        b |= a & (1 << x); // Установить бит x в b, если он был в a
        b |= (a >> x) & 1; // Установить младший бит в b, если в a был бит x
        

        Некоторые путаются.

    • »
      »
      »
      8 лет назад, # ^ |
      Rev. 2   Проголосовать: нравится +30 Проголосовать: не нравится

      Про 8: std::bitset является длинной битовой маской фиксированного (на стадии компиляции) размера. С ней можно делать разные операции за (тут l — размер маски, w — длина машинного слова, т.е. 32/64). Например:

      const int MAXN = int(1e5);
      bitset<MAXN> a, b;
      
      a.set(10); // установить бит 10
      for (int i = 100; i < 110; i++) { // и биты [100..109]
        a.set(i);
      }
      
      b |= a; // установить в b все биты, установленные в a
      

      Алгоритмы, которые сводятся к каким-то простым операциям с длинными масками (побитовое "или", скалярное произведение и прочее), этой штукой оптимизируются. Алгоритм Флоида (имелся ввиду для подсчёта достижимости, а не расстояний):

      // Стандартная версия
      vector<vector<bool>> g = ...;
      for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
          for (int k = 0; k < n; k++)
            g[j][k] = g[j][k] || (g[j][i] && g[i][k]);
      
      // Чуть развернём условия, чтобы в самом вложенном месте остались операции попроще
      for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++)
          if (g[j][i])
            for (int k = 0; k < n; k++)
              g[j][k] = g[j][k] || g[i][k];
      }
      
      // И последняя
      vector<bitset<MAXN>> g = ...;
      for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++)
          if (g[j][i])
            g[j] |= g[i];
      }
      
    • »
      »
      »
      8 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

      12 расписано, например, тут, тут и тут (гуглится по запросу "fast inverse square root"). Кратко: магическая константа нужна лишь для хорошего первого приближения, дальше используется одна итерация метода Ньютона, которая уточнает результат. Магическая константа пользуется тем, что в вещественных числах отдельно хранятся мантисса и экспонента, а в качестве первого приближения корня можно просто поделить экспоненту пополам и сменить у неё знак. По ссылкам более подробно рассказывается, почему константа именно такая (по второй ссылке — какие еще бывают, например, для кубических корней).

    • »
      »
      »
      8 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

      9: Сложение за O(w). Для знаковых тоже сработает (просто потому что знаковые числа хранятся как соответствующие вычеты в кольце , поэтому сложению неважен знак), но на плюсах переполнение в знаковых типпах — undefined behavior, что нехорошо.

      unsigned a = ..., b = ...; // unsigned ~~ unsigned int
      
      while (b) {
        unsigned new_result = a ^ b; // результат без учёта переносов
        unsigned carry = a & b; // из каких разрядов случились переносы (если просто складывали поразрядно)
        // на следующей итерации надо прибавить переносы
        a = new_result;
        b = carry << 1; // переносы случаются из младших разрядов в старшие
      }
      

      Время работы такое, потому что в худшем случае у нас будет один-единственный перенос, который мы будем протаскивать по всему числу, например, (2k - 1) + 1.

      За описано либо на вики, либо можно так: сначала посчитаем, в какие разряды будут переносы (при нормальном сложении с учётом всех переносов), а потом за одну операцию проксорим исходные два числа и переносы.

      Перенос в разряд i случается, если сумма кусков чисел, составленных из всех битов до i-го (исключительно) не меньше 2i. Это что-то сложное, давайте лучше научимся быстро сравнивать начала двух чисел на "больше" и "меньше либо равно": a и ~b. Здесь ~b — это максимальное такое число, что ни на каком префиксе при сложении с b переносов нет. Соответственно, если какой-то префикс a больше соответствующего префикса b, возникнет перенос. А сравнивать мы умеем деревом отрезков. Для схем это делается совсем просто (за глубину ), возможно, я наврал, что это делается для битовых операций — надо подумать.

  • »
    »
    8 лет назад, # ^ |
      Проголосовать: нравится +13 Проголосовать: не нравится

    В Java нет операции <<<

    • »
      »
      »
      8 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится

      Действительно, спасибо. Написал по аналогии. Интересно, что бы она могла делать.

»
8 лет назад, # |
  Проголосовать: нравится +10 Проголосовать: не нравится

Поищи книгу Алгоритмические трюки для программистов, там полно трюков с битовыми операциями.

  • »
    »
    8 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Да-да, знаю про неё. Но хочется посмотреть на именно те трюки, которые люди используют.

»
8 лет назад, # |
Rev. 2   Проголосовать: нравится +16 Проголосовать: не нравится

По множеству масок посчитать все маски, которые содержат хотя бы одну из них:

for (int mask : v)
    dp[mask >> 5] |= (1 << (mask & 31));

for (int mask = 0; mask < (1 << (let - 5)); mask++)
{
    dp[mask] |= (dp[mask] & 0x0000ffff) << 16;
    dp[mask] |= (dp[mask] & 0x00ff00ff) << 8;
    dp[mask] |= (dp[mask] & 0x0f0f0f0f) << 4;
    dp[mask] |= (dp[mask] & 0x33333333) << 2;
    dp[mask] |= (dp[mask] & 0x55555555) << 1;

    for (int j = 0; j < let - 5; j++)
        dp[mask | (1 << j)] |= dp[mask];
}

Смысл состоит в том, что мы храним по 25 значений динамики "удовлетворяет ли названному условию?" в каждом элементе массива, таким образом биты int-ов соответствуют последним 5 битам маски, номер в массиве оставшимся битам. Переходы по "внешним" битам тривиальны, а по "внутренним" выражаются с помощью битовых операций.

P. S. Константы в двоичной записи выглядят так: 00000000000000001111111111111111, 00000000111111110000000011111111, 00001111000011110000111100001111, 00110011001100110011001100110011, 01010101010101010101010101010101, то есть мы выделяем среди наших состояний те, в которых нет 4-го, 3-его, 2-го, 1-го, 0-го битов и делаем из них соответствующие переходы.

»
8 лет назад, # |
  Проголосовать: нравится +37 Проголосовать: не нравится

Zero is not a power of two. Your code says otherwise :)

»
8 лет назад, # |
  Проголосовать: нравится +21 Проголосовать: не нравится

Я верю в то, что битовые трюки за меня делает -O2 :)

»
8 лет назад, # |
Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

Например, https://en.wikipedia.org/wiki/Hamming_weight

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

»
8 лет назад, # |
  Проголосовать: нравится -8 Проголосовать: не нравится

Минимум среди (x, y) = y ^ ((x ^ y) & -(x < y))
Когда x<y: y ^ ((x ^ y) & -(x < y)) = y ^ ((x ^ y) & -1) = y ^ (x ^ y) = x
Когда x>y: y ^ ((x ^ y) & -(x < y)) = y ^ ((x ^ y) & 0) = y ^ 0 = y

»
8 лет назад, # |
Rev. 2   Проголосовать: нравится 0 Проголосовать: не нравится

or:
f = __builtin_popcount(v) == 1;

»
8 лет назад, # |
  Проголосовать: нравится +18 Проголосовать: не нравится

Всё написано до нас: link

  • »
    »
    8 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

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

»
8 лет назад, # |
  Проголосовать: нравится +23 Проголосовать: не нравится

Smallest bit set to 1 of an integer

int bit = number & -number;

Explanation: -number representation using 2-complement keeps the smallest 1-bit and inverts the other most significative bits.

Unsetting the smallest 1-bit of an integer

int number = 123; number -= number & -number;

Counting the number of bits set to 1

int amount = 0;
for (; number > 0; number -= number & -number)
  ++amount;

Toggle bit i from 1->0 or 0->1

number ^= 1 << i;

Xor with bit set to 1, makes it change its value, whereas the other bits are 0 and don't change anything.

Iterate all subsets of a bitmask in increasing order

for ( int sub = 0 ; sub = sub - bitmask & bitmask ; ) {
  // do something
}

Iterate all subsets of a bitmask in decreasing order

for(int sub = (mask-1) & bitmask ; sub > 0 ; sub = (sub-1) & bitmask )  {
  // do something 
}
  • »
    »
    8 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Can u plz give a small example for "Iterate all subsets of a bitmask in increasing order". It is not clear to me... :/

»
8 лет назад, # |
Rev. 2   Проголосовать: нравится +36 Проголосовать: не нравится

Один из классических рюкзаков, а точнее Subset sum problem, на битсетах:

bitset<MAXS> d;
d[0] = 1;
int n;
cin >> n;
while (n--) {
    int x;
    cin >> x;
    d |= d << x;
}
int s;
cin >> s;
cout << d[s] << endl;

И работает быстро, и пишется просто.

»
8 лет назад, # |
Rev. 2   Проголосовать: нравится +13 Проголосовать: не нравится

Оставлю тут pdf

Из того, о чём ещё не писали, там, например, есть рюкзак с восстановлением ответа и Гаусс.

»
8 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Задача для затравки: Найти младший единичный бит за O(1)

А номер младшего за O(1) умеешь? ;)

  • »
    »
    8 лет назад, # ^ |
      Проголосовать: нравится +15 Проголосовать: не нравится

    Я умею с таблицей предподсчёта, взяв число по модулю 37. Работает для 32-битных чисел. Для 64-битных нужно 67. Забегая вперёд -- для __int128 достаточно 131 :)

  • »
    »
    8 лет назад, # ^ |
    Rev. 2   Проголосовать: нравится -8 Проголосовать: не нравится

    Выбирайте:

    //Bit scan (forward):
    
    unsigned Bsf(unsigned x) {
        return __builtin_ctz(x);//Count trailing zeroes.
    }
    
    unsigned Bsf(unsigned x) {
        __asm("bsfl %0, %0" : "+r"(x));
        return x;
    }
    
    unsigned Bsf(unsigned x) {
        unsigned result = 0;
        if (!(x & 0xFFFF)) {
            result = 16;
            x >>= 16;
        }
        if (!(x & 0xFF)) {
            result |= 8;
            x >>= 8;
        }
        if (!(x & 0xF)) {
            result |= 4;
            x >>= 4;
        }
        if (!(x & 0x3)) {
            result |= 2;
            x >>= 2;
        }
        if (!(x & 0x1))
            result++;
        return result;
    }
    
»
8 лет назад, # |
Rev. 12   Проголосовать: нравится -37 Проголосовать: не нравится

Swap two integers

a ^= b ^= a ^= b;

Check whether two integers are equal or not:

if (a ^ b) // they aren't equal!

Check if a equals -1 or not

if (~a) // a != -1

Get the largest number I can!

int (or long long) INF = -1ull/2;

For the least number

int (or long long) LEAST = ~(-1ull/2);
»
8 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится

Operation x & (x — 1) sets rightmost 1 to 0. As powers of 2 have only one 1 — it can be used to check them.

»
8 лет назад, # |
  Проголосовать: нравится +3 Проголосовать: не нравится

My favorite is binary searching by bit manipulation — no off-by-1 errors!

/**
   * Binary search by bit toggling from MSB to LSB
   * O(64) bit-wise operations for Longs (O(32) for Ints)
   *
   * @return Some(x) such that x is the largest number for which f is true
   *         If no such x is found, None
   */
  def bitBinSearch(f: Long => Boolean): Option[Long] = {
    var p = 0L
    var n = Long.MinValue
    var t = n >>> 1
    while (t > 0) {
      if (f(p|t)) p |= t
      if (f(n|t)) n |= t
      t >>= 1
    }
    Seq(p, n) find f
  }

Original source: https://github.com/pathikrit/scalgos/tree/master/src/main/scala/com/github/pathikrit/scalgos

  • »
    »
    8 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Could you explain please what Seq(p, n) find f means?

    • »
      »
      »
      8 лет назад, # ^ |
      Rev. 3   Проголосовать: нравится +4 Проголосовать: не нравится

      p and n are numbers in [0, Long.MaxValue] and [Long.MinValue, 0] respectively for which f maybe satisfactory. Seq(p, n) creates a sequence of these 2 numbers and Seq(p, n) find f returns the first one for which f is true else None. I am using Scala as my language; here's a Java/C++-ish version (not tested):

      long p = 0L, n = Long.MinValue;
      for (long t = n >>> 1; t > 0; t >>= 1) {
        if (f(p|t)) p |= t;
        if (f(n|t)) n |= t;
      }
      if (f(p)) return p;
      if (f(n)) return n;
      return null;
      
»
8 лет назад, # |
  Проголосовать: нравится +20 Проголосовать: не нравится

Дико прошу прощения, если мой коммент покажется неконструктивным, но опасаюсь, как бы вся Ваша статься не получилась ещё более неконструктивной.

Такие вещи позволительно писать только когда есть чёткое описание возможных подвохов. Хотя бы тот же классический обмен двух переменных без третьей — по моему глубокому убеждению, надо либо обязательно указывать, что оно работает только при &x!=&y (а при &x==&y приводит к утрате значения этой переменной), либо вообще не рассказывать о таком! И это касается не только конкретно обмена, а вообще всего.

  • »
    »
    8 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    Да, спасибо, постараюсь это учесть. Казалось бы из всех трюков только 3 — 4 вызывают опасения.

»
6 лет назад, # |
  Проголосовать: нравится +1 Проголосовать: не нравится

Can someone provide links to the problems, where you need a fast way to count number of 1's in mask or index of last setted bit?