Meet in the Middle

Встреча посередине

e-olymp 11431

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 варианта

a? Да Нет b? b? c? c? c? c? {a,b,c} {a,b} {a,c} {a} {b,c} {b} {c} {} 8 листьев = 2^3 подмножеств

n элементов → 2n подмножеств

n = 40 → 240 подмножеств. Сколько это?

240 = 1 099 511 627 776

≈ 1012триллион

При 108 операций/сек → ~10 000 секунд

10 000 секунд = 2 часа 47 минут

Лимит времени: 1 секунда

TLE

А что если n было бы 20?

n = 40n = 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

Колода A:
суммы левой половины

B (sorted)

Колода B:
суммы правой половины
(отсортированы!)

❌ Наивный: сравнить каждую с каждой → m2 пар

✔ Умный: отсортировать правую → m · log(m)

💡 Ключевая идея
Meet-in-the-middle — это та же идея, только «колоды» — это множества всех подмножеств каждой половины

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

Подмножество из n элементов ↔ двоичное число из n бит

Бит i = 1 → элемент i включён   |   Бит i = 0 → не включён

Маска (10)Маска (2)ПодмножествоСумма
0000{}0
1001{a}a
2010{b}b
3011{a, b}a + b
4100{c}c
5101{a, c}a + c
6110{b, c}b + c
7111{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];
}
  

Ключевая строка: sums[mask | (1 << i)] = sums[mask] + arr[i]

Пример: genSums для массива [3, 1, 4]

arr = [3, 1, 4]   (индексы 0, 1, 2)

Начало: sums[000] = 0 (пустое множество)

i = 0 (arr[0] = 3): mask = 0 → sums[001] = sums[000] + 3 = 3    {} → {3}
i = 1 (arr[1] = 1):
mask=0: sums[010] = sums[000] + 1 = 1   {} → {1}
mask=1: sums[011] = sums[001] + 1 = 3+1 = 4   {3} → {3,1}
i = 2 (arr[2] = 4):
mask=0: sums[100] = sums[000] + 4 = 4   {} → {4}
mask=1: sums[101] = sums[001] + 4 = 3+4 = 7   {3} → {3,4}
mask=2: sums[110] = sums[010] + 4 = 1+4 = 5   {1} → {1,4}
mask=3: sums[111] = sums[011] + 4 = 4+4 = 8   {3,1} → {3,1,4}
Инд.МаскаПодмн.Сумма
0000{}0
1001{3}3
2010{1}1
3011{3,1}4
4100{4}4
5101{3,4}7
6110{1,4}5
7111{3,1,4}8

Почему это работает: «удвоение» на каждом шаге

Начало После i=0 После i=1 После i=2 1 2 4 8 ×2 ×2 ×2

Каждый новый элемент удваивает число подмножеств:
было 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 из Aneed = 10 − aВхождений need в BВклад
01000
3722
1911
4600
4600
7300
5511
8211

ans = 0 + 2 + 1 + 0 + 0 + 0 + 1 + 1 = 5

5 подмножеств с суммой 10

Проверка: 5 подмножеств с суммой 10

#SLSRsum(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

0 2 5 7 7 9 12 14 0 1 2 3 4 5 6 7 lower_bound upper_bound 5 - 3 = 2 вхождения

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);
}
  
genSums — генерация sort — сортировка цикл — комбинирование

Разбор: чтение данных и разделение массива


int h = n / 2;
vector<long long> left(arr.begin(), arr.begin() + h);
vector<long long> right(arr.begin() + h, arr.end());
  
arr: индексы 0 ... n-1,   h = n/2 left = arr[0..h-1] right = arr[h..n-1] h
⚠️ Важно
Если 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)!) — допустимо.
Разбиваем задачу на две половины, решаем отдельно, комбинируем результаты.

Характерные признаки:

  1. n ≈ 30–50 (слишком много для 2n, достаточно для 2n/2)
  2. Ответ зависит от двух независимых частей, которые можно скомбинировать
  3. Комбинирование можно сделать быстро (сортировка + бин. поиск, хеш-таблица, два указателя)
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 · 109long 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 ≈ 1012long 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, задача на перебор, ответ можно собрать из двух частей

e-olymp 11431 — Встреча посередине

Попробуйте сдать!

Вопросы?

https://www.e-olymp.com/ru/problems/11431

Домашнее задание: сдать задачу на e-olymp