EduBrick

Сколько стоит перебор

Таблица замеров: что успевает перебраться за секунду. И что делать, когда ограничения чуть больше, чем позволяет перебор.

4 мин

Перед тем как писать перебор, полезно знать, влезет ли он. Ответ определяется одним числом — количеством объектов, — и таблицей ниже.

Замеры

На одной машине, GCC с -O2, пустое тело цикла:

перебор количество время
2202^{20} масок 10610^6 3 мс
2242^{24} масок 1.71071.7 \cdot 10^7 31 мс
2262^{26} масок 6.71076.7 \cdot 10^7 98 мс
10!10! перестановок 3.61063.6 \cdot 10^6 9 мс
11!11! перестановок 41074 \cdot 10^7 95 мс
12!12! перестановок 4.81084.8 \cdot 10^8 1120 мс

С содержательной обработкой каждого варианта время растёт в несколько раз, поэтому запас нужен минимум десятикратный.

Рабочие границы

объект сколько их предел
подмножества 2n2^n n25n \le 25
перестановки n!n! n11n \le 11
сочетания (nk)\binom{n}{k} зависит от kk считать явно
разбиения p(n)p(n) p(60)106p(60) \approx 10^6 n70n \le 70
скобочные, число Каталана C206.6109C_{20} \approx 6.6 \cdot 10^9 n15n \le 15
строки длины nn из kk букв knk^n kn107k^n \le 10^7

Обратите внимание на сочетания: (302)=435\binom{30}{2} = 435, а (3015)=155117520\binom{30}{15} = 155\,117\,520. Одна и та же формулировка «выберите kk из nn» может быть и мгновенной, и безнадёжной — надо считать.

Ограничения подсказывают метод

Обратная задача — по ограничениям угадать ожидаемое решение. Обычно это работает:

nn в условии что от вас ждут
10\le 10 перестановки, полный перебор
20\le 20 подмножества, динамика по маскам 2nn2^n \cdot n
40\le 40 встреча посередине
100\le 100 кубическая динамика или перебор с сильными отсечениями
5000\le 5000 квадрат
105\le 10^5 nlognn \log n
107\le 10^7 линия

Строка «40\le 40» самая характерная: такое ограничение почти никогда не случайно.

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

Приём для nn около 40, когда 2n2^n недостижимо, а 2n/22^{n/2} — вполне.

Идея: разбить объекты на две половины по n/2n/2, перебрать все подмножества каждой отдельно (2201062^{20} \approx 10^6 штук), а затем склеить половины бинарным поиском или двумя указателями.

// суммы всех подмножеств первой половины
vector<long long> first;
for (int mask = 0; mask < (1 << half); mask++) {
    long long sum = 0;
    for (int i = 0; i < half; i++) if (mask >> i & 1) sum += a[i];
    first.push_back(sum);
}
sort(first.begin(), first.end());

// для каждой суммы второй половины ищем дополнение
for (int mask = 0; mask < (1 << (n - half)); mask++) {
    long long sum = 0;
    for (int i = 0; i < n - half; i++) if (mask >> i & 1) sum += a[half + i];
    answer += upper_bound(first.begin(), first.end(), target - sum)
            - lower_bound(first.begin(), first.end(), target - sum);
}

Итог — O(2n/2n)O(2^{n/2} \cdot n) вместо O(2n)O(2^n). Для n=40n = 40 это разница между секундой и тысячей лет.

Ограничение метода: половины должны быть независимы, а способ склейки — быстрым. Для сумм это работает; для задач, где элементы взаимодействуют произвольно, — нет.

Что делать, когда не влезает

По убыванию предпочтительности:

  1. Отсечения. Дёшево, иногда достаточно.
  2. Мемоизация. Если разные ветки приходят в одно состояние — перебор превращается в динамику.
  3. Динамика по подмножествам. 2nn2^n \cdot n вместо n!n! — стандартный ход для задачи коммивояжёра и её родственников.
  4. Встреча посередине. Когда nn около 40.
  5. Полиномиальный алгоритм. Часто задача просто не про перебор, и стоит перечитать условие.

Пункт пятый не шутка: ограничение вида n105n \le 10^5 рядом со словом «переберите» означает, что перебирать надо не то, что кажется.

И про замеры

Приведённые числа — ориентир, а не закон. Реальная скорость зависит от машины, компилятора, обращений к памяти и того, что происходит внутри цикла.

Поэтому единственный надёжный способ узнать, влезет ли решение, — запустить его на максимальном тесте локально. Прикидка отвечает на вопрос «стоит ли вообще писать»; замер — на вопрос «пройдёт ли».