Работа с отдельными битами числа: флаги, права доступа, множества и быстрые алгоритмы на них
& | ^ ~ << >>std::bitset в C++.Целое число хранится в двоичной записи: биты нумеруются справа налево с нуля, бит k «весит» 2k. 180 = 128 + 32 + 16 + 4 = 10110100. Длинные двоичные числа неудобно читать, поэтому пишут шестнадцатеричные: одна шестнадцатеричная цифра — ровно 4 бита.
биты 1011 0100 двоичная
hex B 4 0xB4
десятич. 180 11 · 16 + 4
| hex | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| биты | 0000 | 0001 | 0010 | 0011 | 0100 | 0101 | 0110 | 0111 | 1000 | 1001 | 1010 | 1011 | 1100 | 1101 | 1110 | 1111 |
В 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 — всё это шестнадцатеричная запись байтов.
Битовые операции работают с каждым битом отдельно, в отличие от логических && || !, которые смотрят на число целиком (ноль / не ноль).
| Операция | Бит результата равен 1, если… | a = 1011 0100, b = 0000 1111 |
|---|---|---|
a & b — и | оба бита 1 | 0000 0100 |
a | b — или | хотя бы один бит 1 | 1011 1111 |
a ^ b — исключающее или | биты различны | 1011 1011 |
~a — отрицание | бит a равен 0 | 0100 1011 (младшие 8 бит) |
a << k — сдвиг влево | биты сдвигаются на k влево, справа нули; это умножение на 2k | a << 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 '&'.Маска — число, в котором единицы стоят в нужных битах. С маской 1u << k (единица в бите k) делают четыре действия:
| Действие | Запись | Почему работает |
|---|---|---|
| проверить бит k | (x >> k) & 1u или (x & (1u << k)) != 0 | & с маской обнуляет все биты, кроме k |
| установить в 1 | x |= 1u << k; | | с 1 даёт 1, с 0 — оставляет как было |
| сбросить в 0 | x &= ~(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.
Множество чисел от 0 до 31 хранится в одном uint32_t: бит k равен 1, если k входит в множество. Операции над множествами становятся одной битовой операцией — O(1) вместо цикла по элементам:
| Над множествами | Над масками | A = {1, 3, 5, 7}, B = {3, 4, 5, 6} |
|---|---|---|
| k ∈ A | (a >> k) & 1u | 5 ∈ A — да |
| A ∪ B | a | b | {1, 3, 4, 5, 6, 7} |
| A ∩ B | a & b | {3, 5} |
| A \ B | a & ~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 бит: множество букв слова — одно число, общие буквы двух слов — одно &.
У множества из 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 показывает, где его граница.
Найти все простые числа до 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 килобайт: это решето на битовой маске из практической работы.
bool composite[1000001]; внутри main в Windows аварийно завершит программу при запуске. Большой массив объявляют вне функций (глобальным) или с static (раздел 15), а лучше выделяют динамически (раздел 17).0xB4 = 1011 0100 = 180& | ^ ~ << >> работают по битам; битовые операции — над беззнаковыми, маска — 1u << k&, установить — |=, сбросить — &= ~, переключить — ^=&: (x & m) == 0 со скобкамиuint32_t; объединение, пересечение, разность — одна операцияx & (x - 1) сбрасывает младшую единицу: подсчёт битов и проверка степени двойки& 0xC0); раздела 14 — флаги и байты в двоичных файлах; раздела 19 — хеш-функции; раздела 23 — std::bitset и std::set.