Т-Банк

Алгоритмы.

Параллель C

Линейные алгоритмы

1. Когда нужен long long

В обычной среде для задач используются следующие диапазоны значений:

Тип Минимум Максимум
int −2 147 483 648 2 147 483 647
long long −9 223 372 036 854 775 808 9 223 372 036 854 775 807

Для оценки удобно помнить: максимум int — примерно 2 · 10⁹, а long long — примерно 9 · 10¹⁸. Переполнение знакового типа — неопределённое поведение.

Сумма двух int сначала вычисляется как int. Рассмотрим пример, где сумма двух int-ов может не влезть в int:

int x = 2000000000;
int y = 2000000000;

int bad = x + y;

При таком переполнении обычно не возникает ошибки выполнения: программа продолжает работать с неверным результатом. В этом примере на практике часто сохраняется отрицательное число вместо ожидаемых 4 000 000 000. Однако C++ не гарантирует именно такой результат: поведение при переполнении знакового типа не определено.

Чтобы сумма вычислялась как long long, можно сразу хранить слагаемые в этом типе:

long long x = 2000000000;
long long y = 2000000000;
long long sum = x + y;  // 4000000000

Оба слагаемых имеют тип long long, поэтому сложение тоже выполняется в этом типе.

Можно оставить обе переменные типа int, но перед сложением привести одно из слагаемых к long long:

int x = 2000000000;
int y = 2000000000;

long long sum = (long long)x + y;  // 4000000000

Запись (long long)x преобразует значение x в long long. Второе слагаемое автоматически приводится к тому же типу, поэтому сумма вычисляется как long long. Сами переменные x и y остаются типа int. Приведение нужно сделать до сложения: запись (long long)(x + y) не поможет, поскольку сумма в скобках сначала вычислится как int.

Если складываем до 100 000 чисел, каждое по модулю не больше 10⁹, сумма по модулю может достигать 10¹⁴. Элементы помещаются в int, а для суммы нужен long long:

// a — vector<int>
long long sum = 0;
for (int i = 1; i <= n; i++) {
    sum += a[i];
}

У long long тоже есть предел. Во всех примерах ниже предполагаем, что и ответы, и промежуточные вычисления в него помещаются.

2. Ввод и вывод в 1-нумерации

В C++ индексы по-прежнему начинаются с нуля. Для удобной нумерации от 1 создаём n + 1 элемент: используем a[1] ... a[n], а a[0] оставляем служебным.

Программа читает n, затем n чисел и выводит их в том же порядке:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
    int n;
    cin >> n;
    vector<long long> a(n + 1, 0);

    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }

    for (int i = 1; i <= n; i++) {
        cout << a[i] << ' ';
    }
    cout << '\n';
    return 0;
}

Например, для ввода 4 и чисел 8 3 5 2 вывод будет 8 3 5 2. Числа можно разделять пробелами или переводами строк: cin читает их одинаково.

Дальше приведены фрагменты для вставки внутрь main; используются те же заголовки и using namespace std;.

3. Сортировка массива

После ввода и перед выводом добавим:

sort(a.begin() + 1, a.end());

Теперь 8 3 5 2 превратится в 2 3 5 8. Функция sort из <algorithm> меняет сам массив и сортирует по неубыванию: равные элементы могут стоять рядом.

Диапазон задаётся как [начало, конец): начало включается, конец — нет. a.begin() + 1 указывает на a[1], а a.end() — за последний элемент. Поэтому служебный a[0] не участвует.

Если вы хотите отсортировать весь массив (начиная с нулевого элемента), замените a.begin() + 1 на a.begin()

sort(a.begin(), a.end());

Сортировка n элементов выполняется за O(n log n) операций.

4. Префиксные суммы в 1-нумерации

Задача: массив не меняется, но нужно много раз находить сумму на отрезке [l, r].

pref[i] — сумма первых i элементов. Пустой префикс имеет сумму pref[0] = 0:

pref[i] = a[1] + ... + a[i]
pref[i] = pref[i - 1] + a[i]
sum(l, r) = pref[r] - pref[l - 1]

Из суммы до r вычитаем всё, что находится перед l.

vector<long long> pref(n + 1, 0);
for (int i = 1; i <= n; i++) {
    pref[i] = pref[i - 1] + a[i];
}

int l, r;
cin >> l >> r;  // 1 <= l <= r <= n
cout << pref[r] - pref[l - 1] << '\n';
индекс:  0  1  2  3  4
a:       —  8  3  5  2
pref:    0  8 11 16 18

сумма [2, 4] = pref[4] - pref[1] = 18 - 8 = 10

Предподсчет занимает O(n) времени, один запрос — O(1), то есть константное число действий.

Случай l = 1 не требует отдельной обработки: та же формула даёт pref[r] - pref[0], а pref[0] = 0. Дополнительный if не нужен.

5. Разностный массив в 1-нумерации

Задача: много раз прибавить число ко всем элементам отрезка, а итоговый массив вывести после всех изменений.

Разностный массив хранит изменения между соседними элементами:

a[0] = 0
diff[i] = a[i] - a[i - 1]
a[i] = diff[1] + ... + diff[i]

Чтобы прибавить value ко всему [l, r], достаточно изменить две позиции:

diff[l]     += value  — включаем прибавку
diff[r + 1] -= value  — отменяем её после отрезка

Код для уже считанного массива a с a[0] = 0. После массива вводятся число операций q и q троек l r value:

// нам может потребоваться элемент n+1, поэтому сделаем размер n+2
vector<long long> diff(n + 2, 0);
for (int i = 1; i <= n; i++) {
    diff[i] = a[i] - a[i - 1];
}

int q;
cin >> q;
for (int j = 0; j < q; j++) {
    int l, r;
    long long value;
    cin >> l >> r >> value;  // 1 <= l <= r <= n
    diff[l] += value;
    diff[r + 1] -= value;  // здесь может потребоваться n+1-й элемент
}

// Восстанавливаем массив только после всех операций.
for (int i = 1; i <= n; i++) {
    a[i] = a[i - 1] + diff[i];
    cout << a[i] << ' ';
}
cout << '\n';

Одна операция выполняется за O(1), построение и восстановление — за O(n) каждое. Все q операций вместе требуют O(n + q) времени.

6. Многомерные префиксные суммы: три измерения

В одном измерении префикс — отрезок от начала массива. В двух — прямоугольник от угла таблицы. В трёх — параллелепипед от угла объёма.

Пусть a[x][y][z] — число в ячейке, размеры массива — nx, ny, nz. Все координаты начинаются с 1. Тогда p[x][y][z] хранит сумму области:

[1, x] × [1, y] × [1, z]

Если хотя бы одна координата равна нулю, префикс пуст и его сумма равна нулю.

Предподсчет через битовые маски

Битовая маска — число, двоичная запись которого хранит несколько выборов «да / нет». Для трёх координат нужны три бита. Будем записывать их в порядке x, y, z: левый отвечает за x, средний — за y, правый — за z.

При предподсчете бит 1 означает «уменьшить эту координату на один», бит 0 — «оставить как есть». Например, маска 101 выбирает префикс p[x - 1][y][z - 1].

Чтобы получить p[x][y][z], берём значение текущей ячейки и добавляем соседние префиксы. Их пересечения нужно учитывать со знаками: три соседних объёма складываем, попарные пересечения вычитаем, общее пересечение возвращаем. Это принцип включений и исключений.

Маска (число) Биты xyz Число единиц Что учитываем Знак
0 000 0 a[x][y][z] — текущая ячейка +
1 001 1 p[x][y][z - 1] +
2 010 1 p[x][y - 1][z] +
3 011 2 p[x][y - 1][z - 1] −
4 100 1 p[x - 1][y][z] +
5 101 2 p[x - 1][y][z - 1] −
6 110 2 p[x - 1][y - 1][z] −
7 111 3 p[x - 1][y - 1][z - 1] +

Маска 000 — особый случай: значение ячейки берём отдельно. Читать p[x][y][z] как уже готовый префикс нельзя: мы его только вычисляем. Поэтому соседним префиксам соответствуют маски от 1 до 7. Для них нечётное число единиц означает «плюс», чётное — «минус».

В коде выпишем все слагаемые явно, в том же порядке, что и в таблице.

Вложенные vector создают трёхмерный массив, заполненный нулями. Код читает размеры, затем значения: сначала меняется z, затем y, затем x. Сразу строим префиксы, не сохраняя отдельный массив a:

int nx, ny, nz;
cin >> nx >> ny >> nz;

vector<vector<vector<long long>>> p(
    nx + 1, vector<vector<long long>>(
        ny + 1, vector<long long>(nz + 1, 0)));

for (int x = 1; x <= nx; x++) {
    for (int y = 1; y <= ny; y++) {
        for (int z = 1; z <= nz; z++) {
            long long value;
            cin >> value;
            p[x][y][z] = value
                + p[x][y][z - 1]
                + p[x][y - 1][z]
                - p[x][y - 1][z - 1]
                + p[x - 1][y][z]
                - p[x - 1][y][z - 1]
                - p[x - 1][y - 1][z]
                + p[x - 1][y - 1][z - 1];
        }
    }
}

Например, маска 101 означает уменьшение координат x и z на один. Единиц две, поэтому вычитаем p[x - 1][y][z - 1]. Все используемые префиксы уже вычислены: хотя бы одна координата меньше текущей, остальные не больше неё.

Вычисление суммы через битовые маски

Теперь найдём сумму области [x1, x2] × [y1, y2] × [z1, z2]. Все границы включены и лежат внутри массива.

Используем те же три бита, но каждый теперь выбирает границу префикса:

Берём большой объём, вычитаем три лишние области, возвращаем их попарные пересечения и вычитаем общее пересечение:

Маска (число) Биты xyz Число единиц Что учитываем Знак
0 000 0 p[x2][y2][z2] +
1 001 1 p[x2][y2][z1 - 1] −
2 010 1 p[x2][y1 - 1][z2] −
3 011 2 p[x2][y1 - 1][z1 - 1] +
4 100 1 p[x1 - 1][y2][z2] −
5 101 2 p[x1 - 1][y2][z1 - 1] +
6 110 2 p[x1 - 1][y1 - 1][z2] +
7 111 3 p[x1 - 1][y1 - 1][z1 - 1] −

Здесь учитываем все маски от 0 до 7. Чётное число единиц означает «плюс», нечётное — «минус». Это обратные знаки по сравнению с ненулевыми масками при предподсчете. Снова выпишем слагаемые в порядке строк таблицы:

int x1, y1, z1, x2, y2, z2;
cin >> x1 >> y1 >> z1 >> x2 >> y2 >> z2;

long long answer = p[x2][y2][z2]
    - p[x2][y2][z1 - 1]
    - p[x2][y1 - 1][z2]
    + p[x2][y1 - 1][z1 - 1]
    - p[x1 - 1][y2][z2]
    + p[x1 - 1][y2][z1 - 1]
    + p[x1 - 1][y1 - 1][z2]
    - p[x1 - 1][y1 - 1][z1 - 1];

cout << answer << '\n';

Например, маска 101 выбирает p[x1 - 1][y2][z1 - 1]. Единиц две, поэтому этот префикс прибавляем.

В массиве 2 × 2 × 2 из единиц сумма области [2, 2] × [1, 2] × [1, 2] равна 8 - 4 = 4: маска 000 добавит 8, маска 100 вычтет 4. Остальные маски дадут нулевые префиксы. Если нижняя граница равна 1, отдельная обработка не нужна: соответствующая координата префикса станет нулевой.

Предподсчет занимает O(nx · ny · nz) времени: для каждой ячейки учитываем 7 соседних префиксов. Один запрос занимает O(1): в формуле всегда 8 префиксов.