IOI Prep Camp — День 1 | Аббас Алиев
28 апреля 2026
Условие задачи
e-olymp 11431 — Встреча посередине
Дан массив из n целых чисел.
Сколькими способами можно выбрать подмножество элементов с суммой ровно s?
Ограничения: n ≤ 40 • s ≤ 109 • ti ≤ 109
Пример
Массив: [3, 1, 4, 2], s = 5
Ответ: 2 (подмножества {3, 2} и {1, 4})
Первая мысль: перебрать все подмножества
Каждый элемент: взять или не взять — 2 варианта
n элементов → 2n подмножеств
n = 40 → 240 подмножеств. Сколько это?
240 = 1 099 511 627 776
≈ 1012 — триллион
При 108 операций/сек → ~10 000 секунд
10 000 секунд = 2 часа 47 минут ⏰
Лимит времени: 1 секунда ❗
TLE
А что если n было бы 20?
n = 40
n = 20
Подмножеств
240 ≈ 1012
220 ≈ 106
Время
~10 000 сек
< 0.01 сек
Вердикт
TLE
OK
106 — это мгновенно! Как это использовать?
Идея: разрежь массив пополам
a1
a2
...
a20
a21
a22
...
a40
✂
a1
a2
...
a20
Левая: 20 элементов
220 ≈ 106 — БЫСТРО!
a21
a22
...
a40
Правая: 20 элементов
220 ≈ 106 — БЫСТРО!
Любое подмножество = левая часть + правая часть
S = SL ∪ SR
sum(S) = sum(SL) + sum(SR)
Пример: [3, 1, 4 | 2, 5, 7], s = 10
Подмножество {3, 7}:
SL = {3}, SR = {7}
sum = 3 + 7 = 10 ✔
Подмножество {1, 4, 5}:
SL = {1, 4}, SR = {5}
sum = 5 + 5 = 10 ✔
💡 Ключевая идея
Нам нужно: sum(SL) + sum(SR) = s
То есть: sum(SR) = s − sum(SL)
Алгоритм Meet in the Middle: 3 шага
Шаг 1: Генерация — Перебрать все 2n/2 подмножеств левой половины, записать их суммы в массив A
Шаг 2: Генерация + Сортировка — Перебрать все 2n/2 подмножеств правой половины, записать их суммы в массив B. Отсортировать B.
Шаг 3: Комбинирование — Для каждого a из A бинарным поиском найти, сколько элементов B равны (s − a). Суммировать.
Итоговая сложность
Полный перебор: 240 ≈ 1012
↓
Левая: 220 ≈ 106
Правая: 220 ≈ 106
↓
Комбинирование: 106 · log(106) ≈ 2·107
Итого: ~2·107 вместо 1012
Аналогия: две колоды карт
Колода A: суммы левой половины
↔
Колода B: суммы правой половины (отсортированы!)
❌ Наивный: сравнить каждую с каждой → m2 пар
✔ Умный: отсортировать правую → m · log(m)
💡 Ключевая идея
Meet-in-the-middle — это та же идея, только «колоды» — это множества всех подмножеств каждой половины
Перебор подмножеств через битовые маски
Подмножество из n элементов ↔ двоичное число из n бит
Бит i = 1 → элемент i включён | Бит i = 0 → не включён
Маска (10)
Маска (2)
Подмножество
Сумма
0
000
{}
0
1
001
{a}
a
2
010
{b}
b
3
011
{a, b}
a + b
4
100
{c}
c
5
101
{a, c}
a + c
6
110
{b, c}
b + c
7
111
{a, b, c}
a + b + c
23 = 8 масок = 8 подмножеств — ни одно не пропущено и нет повторов
Нужные битовые операции
1.1 << i — число с единственной единицей в позиции i 1 << 0 = 1 = 1
1 << 1 = 10 = 2
1 << 2 = 100 = 4
2.mask | (1 << i) — установить бит i 011 | 100 = 111 (добавили элемент 2)
3.mask & (1 << i) — проверить, установлен ли бит i 101 & 010 = 000 (бит 1 не установлен)
4.1 << n — это 2n (количество всех масок) 1 << 3 = 1000 = 8
Генерация сумм: наивный способ
// Наивная версия — понятная, но медленнее
vector<long long> sums;
int n = arr.size();
for (int mask = 0; mask < (1 << n); mask++) {
long long sum = 0;
for (int i = 0; i < n; i++) {
if (mask & (1 << i)) // бит i установлен?
sum += arr[i];
}
sums.push_back(sum);
}
if (mask & (1 << i)) — «Проверяем каждый бит маски»
Сложность: O(2n · n) — для каждой маски перебираем все n бит
Это работает, но можно быстрее!
Умная генерация: используем уже вычисленные суммы
💡 Ключевая идея
Сумма подмножества с маской mask | (1 << i) = сумма подмножества с маской mask + arr[i]
«подмножество с элементом i» = «подмножество без элемента i» + arr[i]
void genSums(vector<long long>& arr, vector<long long>& sums) {
int n = arr.size();
sums.resize(1 << n);
sums[0] = 0; // пустое подмножество
for (int i = 0; i < n; i++)
for (int mask = 0; mask < (1 << i); mask++)
sums[mask | (1 << i)] = sums[mask] + arr[i];
}
Каждый новый элемент удваивает число подмножеств:
было k — стало 2k
Полный пример: [3, 1, 4, 2, 5, 7], s = 10
Шаг 0: Разделяем
3
1
4
|
2
5
7
Шаг 1: Суммы левой половины → массив A
Подмн.
Сумма
Подмн.
Сумма
{}
0
{4}
4
{3}
3
{3,4}
7
{1}
1
{1,4}
5
{3,1}
4
{3,1,4}
8
A = [0, 3, 1, 4, 4, 7, 5, 8]
Шаг 2: Суммы правой половины + сортировка
Подмн.
Сумма
Подмн.
Сумма
{}
0
{7}
7
{2}
2
{2,7}
9
{5}
5
{5,7}
12
{2,5}
7
{2,5,7}
14
B до сортировки: [0, 2, 5, 7, 7, 9, 12, 14]
B после сортировки: [0, 2, 5, 7, 7, 9, 12, 14]
0
2
5
7
7
9
12
14
Два элемента 7 рядом — одинаковые суммы от разных подмножеств ({2,5} и {7}) — это нормально!
Шаг 3: Комбинирование — ищем пары
A = [0, 3, 1, 4, 4, 7, 5, 8] B = [0, 2, 5, 7, 7, 9, 12, 14] s = 10, need = 10 − a
a из A
need = 10 − a
Вхождений need в B
Вклад
0
10
0
0
3
7
2
2
1
9
1
1
4
6
0
0
4
6
0
0
7
3
0
0
5
5
1
1
8
2
1
1
ans = 0 + 2 + 1 + 0 + 0 + 0 + 1 + 1 = 5
5 подмножеств с суммой 10
Проверка: 5 подмножеств с суммой 10
#
SL
SR
sum(SL) + sum(SR)
Подмножество
1
{3}
{2, 5}
3 + 7 = 10
{3, 2, 5}
2
{3}
{7}
3 + 7 = 10
{3, 7}
3
{1}
{2, 7}
1 + 9 = 10
{1, 2, 7}
4
{1, 4}
{5}
5 + 5 = 10
{1, 4, 5}
5
{3, 1, 4}
{2}
8 + 2 = 10
{3, 1, 4, 2}
✔ Все 5 правильные!
Как считать количество: lower_bound и upper_bound
B = [0, 2, 5, 7, 7, 9, 12, 14] Ищем: need = 7
long long need = s - a;
ans += upper_bound(B.begin(), B.end(), need)
- lower_bound(B.begin(), B.end(), need);
💡 Запомнить
lower_bound(x) → первый элемент ≥ x upper_bound(x) → первый элемент > x
Разница = количество элементов, равных x
Полное решение (C++)
#include <bits/stdc++.h>
using namespace std;
void genSums(vector<long long>& arr, vector<long long>& sums) {
int n = arr.size();
sums.resize(1 << n);
sums[0] = 0;
for (int i = 0; i < n; i++)
for (int mask = 0; mask < (1 << i); mask++)
sums[mask | (1 << i)] = sums[mask] + arr[i];
}
int main() {
int n;
long long s;
scanf("%d %lld", &n, &s);
vector<long long> arr(n);
for (int i = 0; i < n; i++) scanf("%lld", &arr[i]);
int h = n / 2;
vector<long long> left(arr.begin(), arr.begin() + h);
vector<long long> right(arr.begin() + h, arr.end());
vector<long long> A, B;
genSums(left, A);
genSums(right, B);
sort(B.begin(), B.end());
long long ans = 0;
for (long long a : A) {
long long need = s - a;
ans += upper_bound(B.begin(), B.end(), need)
- lower_bound(B.begin(), B.end(), need);
}
printf("%lld\n", ans);
}
int h = n / 2;
vector<long long> left(arr.begin(), arr.begin() + h);
vector<long long> right(arr.begin() + h, arr.end());
⚠️ Важно
Если n нечётное (например, n = 7): h = 3, left = 3 элемента, right = 4 элемента.
Это нормально: 23 + 24 = 8 + 16 = 24 — всё ещё быстро.
Лучше правая половина чуть больше, чем левая (221 > 219, но обе быстрые).
Ловушка: типы данных
⚠️ Осторожно: переполнение!
Суммы могут быть до 20 · 109 (20 элементов по 109)
int вмещает до ~2 · 109 — переполнение!
Нужен long long для сумм и для s
long long s; // не int!
vector<long long> arr(n); // не int!
// В genSums:
vector<long long>& sums; // не int!
⚠️ Особенно опасно
1 << n при n ≥ 31 → переполнение int!
Решение: 1LL << n или (long long)1 << n
Всегда проверяйте: может ли сумма/произведение переполнить int?
Анализ сложности
Этап
Операций
Пояснение
genSums(left)
2n/2
Генерация сумм левой половины
genSums(right)
2n/2
Генерация сумм правой половины
sort(B)
2n/2 · n/2
Сортировка
Цикл + бин. поиск
2n/2 · n/2
Для каждого a поиск в B
Итого: O(2n/2 · n/2)
Для n = 40: 220 · 20 = 1 048 576 · 20 ≈ 2 · 107
1012
Полный перебор
→
2 · 107 ✔
Meet in the Middle
Ускорение: в 50 000 раз
А хватит ли памяти?
Два массива A и B, каждый размером 2n/2 = 220 ≈ 106
Каждый элемент — long long = 8 байт
Итого: 2 · 106 · 8 = 16 МБ
Типичный лимит памяти: 256 МБ
✔
OK — с огромным запасом!
Когда применять Meet in the Middle?
💡 Ключевая идея — Паттерн
Полный перебор O(2n) или O(n!) слишком медленный, но O(2n/2) или O((n/2)!) — допустимо. Разбиваем задачу на две половины, решаем отдельно, комбинируем результаты.
Характерные признаки:
n ≈ 30–50 (слишком много для 2n, достаточно для 2n/2)
Ответ зависит от двух независимых частей, которые можно скомбинировать
Комбинирование можно сделать быстро (сортировка + бин. поиск, хеш-таблица, два указателя)
n ≤ 20 Полный перебор 2n ≈ 106
n = 21..40 Meet in the Middle 2n/2 ≈ 106
n > 40 Другой подход (DP, жадный...)
Где ещё применяется Meet in the Middle?
1. Сумма подмножеств (наша задача) — n ≤ 40, сумма = s
2. 4SUM — даны 4 массива, найти a+b+c+d = 0. Разбиваем на (a+b) и (c+d)
3. Дискретный логарифм (Baby-step Giant-step) — найти x: ax ≡ b (mod p). Разбиваем x = i·m + j
4. Задача о рюкзаке при малом n — n ≤ 40, вместо DP по весу
5. Перебор перестановок — n! слишком много, но можно разбить на две группы
Профессор Медведев разберёт эту технику глубже на своих лекциях
Частые ошибки при реализации
1. Переполнение int:
Суммы до 20 · 109 → long long.
1 << n при n ≥ 31 → переполнение! Пишите 1LL << n
2. Забыли отсортировать B:
Без сортировки lower_bound/upper_bound дают мусор!
Отсортировать нужно ОДИН раз, перед циклом.
3. Неправильное разделение:arr.begin() + h — не arr.begin() + h + 1!
Конструктор vector(begin, end) берёт полуинтервал [begin, end)
4. int для ответа:
Ответ может быть до 240 ≈ 1012 → long long ans
Вариант: unordered_map вместо бинарного поиска
unordered_map<long long, int> cnt;
for (long long b : B)
cnt[b]++;
long long ans = 0;
for (long long a : A)
ans += cnt[s - a];
Сортировка + бин. поиск
unordered_map
Сложность
O(2n/2 · n/2)
O(2n/2) в среднем
Константа
Маленькая
Побольше (хеширование)
Надёжность
Всегда работает
Редко — антихеш-тесты
Рекомендация
На олимпиаде — надёжнее
Для быстрого прототипа
Подведём итоги
1. Суть приёма
240 — это слишком, но 2 × 220 — нормально. Разделяй и комбинируй.
2. Три шага
• Генерируем суммы подмножеств каждой половины (genSums)
• Сортируем одну половину
• Бинарным поиском считаем пары
3. Когда применять
n ≈ 30–50, задача на перебор, ответ можно собрать из двух частей