Линейные алгоритмы
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]. Все границы включены и лежат внутри
массива.
Используем те же три бита, но каждый теперь выбирает границу префикса:
0— верхнюю границу:x2,y2илиz2;1— позицию перед нижней границей:x1 - 1,y1 - 1илиz1 - 1.
Берём большой объём, вычитаем три лишние области, возвращаем их попарные пересечения и вычитаем общее пересечение:
| Маска (число) | Биты 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 префиксов.