Сколько стоит перебор
Таблица замеров: что успевает перебраться за секунду. И что делать, когда ограничения чуть больше, чем позволяет перебор.
4 мин
Перед тем как писать перебор, полезно знать, влезет ли он. Ответ определяется одним числом — количеством объектов, — и таблицей ниже.
Замеры
На одной машине, GCC с -O2, пустое тело цикла:
| перебор | количество | время |
|---|---|---|
| масок | 3 мс | |
| масок | 31 мс | |
| масок | 98 мс | |
| перестановок | 9 мс | |
| перестановок | 95 мс | |
| перестановок | 1120 мс |
С содержательной обработкой каждого варианта время растёт в несколько раз, поэтому запас нужен минимум десятикратный.
Рабочие границы
| объект | сколько их | предел |
|---|---|---|
| подмножества | — | |
| перестановки | — | |
| сочетания | зависит от | считать явно |
| разбиения | ||
| скобочные, число Каталана | ||
| строки длины из букв |
Обратите внимание на сочетания: , а . Одна и та же формулировка «выберите из » может быть и мгновенной, и безнадёжной — надо считать.
Ограничения подсказывают метод
Обратная задача — по ограничениям угадать ожидаемое решение. Обычно это работает:
| в условии | что от вас ждут |
|---|---|
| перестановки, полный перебор | |
| подмножества, динамика по маскам | |
| встреча посередине | |
| кубическая динамика или перебор с сильными отсечениями | |
| квадрат | |
| линия |
Строка «» самая характерная: такое ограничение почти никогда не случайно.
Встреча посередине
Приём для около 40, когда недостижимо, а — вполне.
Идея: разбить объекты на две половины по , перебрать все подмножества каждой отдельно ( штук), а затем склеить половины бинарным поиском или двумя указателями.
// суммы всех подмножеств первой половины
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);
}
Итог — вместо . Для это разница между секундой и тысячей лет.
Ограничение метода: половины должны быть независимы, а способ склейки — быстрым. Для сумм это работает; для задач, где элементы взаимодействуют произвольно, — нет.
Что делать, когда не влезает
По убыванию предпочтительности:
- Отсечения. Дёшево, иногда достаточно.
- Мемоизация. Если разные ветки приходят в одно состояние — перебор превращается в динамику.
- Динамика по подмножествам. вместо — стандартный ход для задачи коммивояжёра и её родственников.
- Встреча посередине. Когда около 40.
- Полиномиальный алгоритм. Часто задача просто не про перебор, и стоит перечитать условие.
Пункт пятый не шутка: ограничение вида рядом со словом «переберите» означает, что перебирать надо не то, что кажется.
И про замеры
Приведённые числа — ориентир, а не закон. Реальная скорость зависит от машины, компилятора, обращений к памяти и того, что происходит внутри цикла.
Поэтому единственный надёжный способ узнать, влезет ли решение, — запустить его на максимальном тесте локально. Прикидка отвечает на вопрос «стоит ли вообще писать»; замер — на вопрос «пройдёт ли».