Раздел 12 — Битовые операции и множества

Работа с отдельными битами числа: флаги, права доступа, множества и быстрые алгоритмы на них

Прогресс курса Раздел 12 из 28

Что вы освоите в этом разделе

2 академических часа теории и 2 часа практики: практическая работа №10 «Работа с данными типа множество» — на портале. В Pascal множество — встроенный тип; в C его делают из битов целого числа — так же устроено std::bitset в C++.
01

Двоичная и шестнадцатеричная запись

Целое число хранится в двоичной записи: биты нумеруются справа налево с нуля, бит k «весит» 2k. 180 = 128 + 32 + 16 + 4 = 10110100. Длинные двоичные числа неудобно читать, поэтому пишут шестнадцатеричные: одна шестнадцатеричная цифра — ровно 4 бита.

  биты     1011 0100           двоичная
  hex        B    4            0xB4
  десятич.  180                11 · 16 + 4
hex0123456789ABCDEF
биты0000000100100011010001010110011110001001101010111100110111101111

В C: литерал 0xB4, вывод printf("%X", x) (или %x — строчными, %02X — две цифры с нулём). Двоичных литералов и формата в C11 нет — двоичную запись печатают своей функцией:

/* Вывести младшие bits битов x, старший первым */
void print_bits(unsigned x, int bits)
{
    for (int k = bits - 1; k >= 0; k--) {
        putchar((x >> k) & 1u ? '1' : '0');
    }
    putchar('\n');
}
                                          /* print_bits(180, 8)  ->  10110100 */

Цвет #FF8000 в CSS, MAC-адрес 00:1A:2B:…, коды символов в UTF-8 — всё это шестнадцатеричная запись байтов.

02

Битовые операции

Битовые операции работают с каждым битом отдельно, в отличие от логических && || !, которые смотрят на число целиком (ноль / не ноль).

ОперацияБит результата равен 1, если…a = 1011 0100, b = 0000 1111
a & b — иоба бита 10000 0100
a | b — илихотя бы один бит 11011 1111
a ^ b — исключающее илибиты различны1011 1011
~a — отрицаниебит a равен 00100 1011 (младшие 8 бит)
a << k — сдвиг влевобиты сдвигаются на k влево, справа нули; это умножение на 2ka << 1 → 1 0110 1000
a >> k — сдвиг вправобиты сдвигаются вправо, младшие пропадают; деление на 2k нацелоa >> 2 → 0010 1101
Биты — только в беззнаковых. Сдвиг вправо отрицательного числа зависит от компилятора, сдвиг влево отрицательного и сдвиг на число бит, не меньшее размера типа (1 << 32 для 32-битного), — неопределённое поведение. Правило курса: битовые операции — над unsigned или типами uint8_t, uint32_t, uint64_t из <stdint.h>; единица в маске — 1u. И приоритеты: x & 1 == 0 — это x & (1 == 0), сравнение выполняется раньше &. Скобки обязательны: (x & 1u) == 0; gcc предупреждает suggest parentheses around comparison in operand of '&'.
03

Маски и флаги

Маска — число, в котором единицы стоят в нужных битах. С маской 1u << k (единица в бите k) делают четыре действия:

ДействиеЗаписьПочему работает
проверить бит k(x >> k) & 1u или (x & (1u << k)) != 0& с маской обнуляет все биты, кроме k
установить в 1x |= 1u << k;| с 1 даёт 1, с 0 — оставляет как было
сбросить в 0x &= ~(1u << k);маска с нулём в бите k и единицами в остальных
переключитьx ^= 1u << k;^ с 1 меняет бит, с 0 — не меняет

Флаги — несколько да/нет-признаков в одном числе. Права файла в Linux — 9 флагов: чтение (r), запись (w), выполнение (x) для владельца, группы и остальных. Каждая тройка — восьмеричная цифра, отсюда chmod 755:

enum {
    READ  = 4,          /* 100 */
    WRITE = 2,          /* 010 */
    EXEC  = 1           /* 001 */
};

unsigned owner = READ | WRITE | EXEC;   /* 7 = rwx */
unsigned group = READ | EXEC;           /* 5 = r-x */
bool can_write = (group & WRITE) != 0;  /* false */
unsigned mode = owner << 6 | group << 3 | group;   /* 0755 = 493 */

Флаги — обычный способ передать несколько настроек в функцию одним параметром: open(path, O_WRONLY | O_CREAT | O_TRUNC) в Linux, стили окна в Windows API и Qt.

04

Множество как битовая маска

Множество чисел от 0 до 31 хранится в одном uint32_t: бит k равен 1, если k входит в множество. Операции над множествами становятся одной битовой операцией — O(1) вместо цикла по элементам:

Над множествамиНад маскамиA = {1, 3, 5, 7}, B = {3, 4, 5, 6}
k ∈ A(a >> k) & 1u5 ∈ A — да
A ∪ Ba | b{1, 3, 4, 5, 6, 7}
A ∩ Ba & b{3, 5}
A \ Ba & ~b{1, 7}
симметрическая разностьa ^ b{1, 4, 6, 7}
A ⊆ B(a & ~b) == 0нет
пустое множествоa == 0

Число элементов — число единичных битов. Приём: x &= x - 1 сбрасывает младшую единицу (вычитание единицы превращает младшую единицу в 0, а нули справа от неё — в единицы, и & их убирает). Цикл выполняется столько раз, сколько единиц:

int count_bits(uint32_t x)
{
    int count = 0;
    while (x != 0) {
        x &= x - 1;               /* 10110100 -> 10110000 -> 10100000 -> 10000000 -> 0 */
        count++;
    }
    return count;
}

Тот же приём: x != 0 && (x & (x - 1)) == 0 — «x — степень двойки» (ровно одна единица). Для множеств больше 32 или 64 элементов берут массив чисел: элемент k — бит k % 32 в слове k / 32. Буквы латинского алфавита — 26 бит: множество букв слова — одно число, общие буквы двух слов — одно &.

05

Перебор подмножеств

У множества из n элементов 2n подмножеств, и каждое — число от 0 до 2n − 1: бит i говорит, взят ли элемент i. Перебрать все подмножества — перебрать все числа:

/* Есть ли среди n чисел подмножество с суммой target? Перебор 2^n масок */
for (uint32_t mask = 1; mask < (1u << n); mask++) {
    long long sum = 0;
    for (int i = 0; i < n; i++) {
        if ((mask >> i) & 1u) {
            sum += a[i];
        }
    }
    if (sum == target) {
        … подмножество найдено: элементы a[i] с единицей в бите i …
    }
}

Это полный перебор: O(2n · n). Для n = 20 — около 20 миллионов операций (доли секунды), для n = 40 — 4 · 1013 (дни). Полный перебор — честный способ решить задачу, у которой нет быстрого алгоритма, но только для небольших n; оценка 2n показывает, где его граница.

06

Решето Эратосфена

Найти все простые числа до n: выписать числа 2…n, взять первое невычеркнутое (оно простое) и вычеркнуть все его кратные; повторить. Вычёркивать кратные p можно начиная с p² — меньшие кратные уже вычеркнуты меньшими делителями; поэтому внешний цикл идёт до √n:

/* composite[k] == true — k составное; заполняется для 0..n */
void sieve(bool composite[], int n)
{
    for (int k = 0; k <= n; k++) {
        composite[k] = k < 2;
    }
    for (int p = 2; p <= n / p; p++) {
        if (!composite[p]) {
            for (int m = p * p; m <= n; m += p) {
                composite[m] = true;
            }
        }
    }
}

Решето делает около n · ln ln n вычёркиваний — почти O(n): для n = 106 это несколько миллионов операций против ~108 при проверке каждого числа делителями до √n (раздел 6). Простых до 100 — 25, до миллиона — 78 498. Массив bool на миллион — мегабайт; если хранить по биту на число, — 125 килобайт: это решето на битовой маске из практической работы.

Массив на миллион элементов не объявляют внутри функции: локальные переменные лежат в стеке, а его размер — обычно 1 МБ в Windows и 8 МБ в Linux. bool composite[1000001]; внутри main в Windows аварийно завершит программу при запуске. Большой массив объявляют вне функций (глобальным) или с static (раздел 15), а лучше выделяют динамически (раздел 17).

Ключевые выводы раздела

Запомните главное

  • Одна шестнадцатеричная цифра — 4 бита; 0xB4 = 1011 0100 = 180
  • & | ^ ~ << >> работают по битам; битовые операции — над беззнаковыми, маска — 1u << k
  • Проверить — &, установить — |=, сбросить — &= ~, переключить — ^=
  • Сравнение выполняется раньше &: (x & m) == 0 со скобками
  • Множество 0…31 — одно uint32_t; объединение, пересечение, разность — одна операция
  • x & (x - 1) сбрасывает младшую единицу: подсчёт битов и проверка степени двойки
  • Подмножества n элементов — числа 0…2n − 1; полный перебор — O(2n)
  • Решето Эратосфена — все простые до n почти за O(n)
Связи раздела. Опирается на раздел 3 — двоичная запись, беззнаковые типы, дополнительный код; раздел 6 — проверка простоты делителями; раздел 8 — массив как таблица счётчиков. Нужен для: раздела 11 — байты UTF-8 (& 0xC0); раздела 14 — флаги и байты в двоичных файлах; раздела 19 — хеш-функции; раздела 23 — std::bitset и std::set.
Практическая работа №10 и тест по разделу 12 — на учебном портале. Задания по вариантам, критерии оценивания и сдача — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 11: Строки в C Все разделы Раздел 13: Структуры и текстовые файлы