Динамика по маскам
Подмножество как число. Экспонента, но приемлемая — до двадцати с небольшим элементов.
4 мин
Когда состояние — это подмножество небольшого набора, его кодируют числом: бит равен единице, если элемент в подмножестве.
Такое состояние занимает значений. При это миллион — приемлемо; при уже 33 миллиона и обычно поздно.
Пример: гамильтонов путь
Найти путь минимального веса, проходящий через все вершины по разу.
Чего не хватает, если хранить только маску посещённых? Того, где мы сейчас стоим: продолжить путь можно только из последней вершины.
— минимальный вес пути, посетившего ровно вершины из маски и закончившегося в .
vector<vector<long long>> dp(1 << n, vector<long long>(n, INF));
for (int v = 0; v < n; v++) dp[1 << v][v] = 0;
for (int mask = 1; mask < (1 << n); mask++)
for (int v = 0; v < n; v++) {
if (dp[mask][v] >= INF || !(mask >> v & 1)) continue;
for (int u = 0; u < n; u++) {
if (mask >> u & 1) continue; // u уже посещена
if (w[v][u] >= INF) continue; // ребра нет
long long& cell = dp[mask | (1 << u)][u];
cell = min(cell, dp[mask][v] + w[v][u]);
}
}
Состояний , переходов из каждого — итого .
Проверено: на 4 000 графов до семи вершин совпало с перебором всех перестановок.
Измерено, сколько это стоит:
| время | ||
|---|---|---|
| 16 | 29 мс | |
| 18 | 135 мс | |
| 20 | 620 мс |
Граница проходит примерно по : дальше не помещается ни во время, ни в память.
Почему порядок обхода масок работает
Маски перебираются по возрастанию числа. Переход mask -> mask | (1 << u) всегда увеличивает число: мы устанавливаем бит, который был нулём.
Значит, к моменту обработки маски все маски, из которых в неё можно попасть, уже обработаны. Топологический порядок получается сам, без всякого топсорта, — и это общая причина, почему динамика по маскам пишется простым for.
Пример: когда позиция не нужна
Есть встреч. На встречу можно прийти, только если текущее настроение лежит в ; после встречи настроение меняется на . Начальное настроение . Максимум встреч.
Здесь номер последней встречи в состояние не нужен. Настроение после набора встреч равно и от порядка не зависит.
Поэтому — просто «достижима ли такая маска», а ответ — маска с наибольшим числом единиц. Состояний вместо .
Вывод общий: прежде чем добавлять параметр в состояние, проверьте, влияет ли он на будущее. Здесь не влиял.
Функция от маски за суммарную линию
Считать перебором битов — это на всё. Можно за .
Возьмём любой установленный бит маски и сведём к маске без него:
vector<long long> sum(1 << n, 0);
for (int mask = 1; mask < (1 << n); mask++) {
int b = __builtin_ctz(mask); // младший установленный бит
sum[mask] = c[b] + sum[mask ^ (1 << b)];
}
__builtin_ctz — число нулей в конце двоичной записи, то есть номер младшего единичного бита. Одна процессорная инструкция.
Проверено: на 20 000 наборов совпало с прямым подсчётом.
В приведённой задаче эта оптимизация не нужна — там доминирует другой цикл. Но встречаются задачи, где подсчёт функции по маскам и есть основная работа.
Типовые применения
| задача | состояние |
|---|---|
| гамильтонов путь, задача коммивояжёра | маска + последняя вершина |
| разбиение множества на группы | маска обработанных |
| паросочетание в двудольном графе с малой долей | маска занятых справа + номер слева |
| раскраска графа в цветов | маска покрашенных |
| покрытие множества подмножествами | маска покрытых |
Во всех случаях ограничение читается прямо из условия: если в задаче написано и речь про подмножества — это маски.