EduBrick

Динамика по маскам

Подмножество как число. Экспонента, но приемлемая — до двадцати с небольшим элементов.

4 мин

Когда состояние — это подмножество небольшого набора, его кодируют числом: бит ii равен единице, если элемент ii в подмножестве.

Такое состояние занимает 2n2^n значений. При n20n \le 20 это миллион — приемлемо; при n=25n = 25 уже 33 миллиона и обычно поздно.

Пример: гамильтонов путь

Найти путь минимального веса, проходящий через все вершины по разу.

Чего не хватает, если хранить только маску посещённых? Того, где мы сейчас стоим: продолжить путь можно только из последней вершины.

dp[mask][v]dp[\text{mask}][v] — минимальный вес пути, посетившего ровно вершины из маски и закончившегося в vv.

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]);
        }
    }

Состояний 2nn2^n \cdot n, переходов из каждого nn — итого O(2nn2)O(2^n n^2).

Проверено: на 4 000 графов до семи вершин совпало с перебором всех перестановок.

Измерено, сколько это стоит:

nn 2nn22^n n^2 время
16 1,71071{,}7\cdot10^7 29 мс
18 8,51078{,}5\cdot10^7 135 мс
20 4,21084{,}2\cdot10^8 620 мс

Граница проходит примерно по n=20n = 20: дальше не помещается ни во время, ни в память.

Почему порядок обхода масок работает

Маски перебираются по возрастанию числа. Переход mask -> mask | (1 << u) всегда увеличивает число: мы устанавливаем бит, который был нулём.

Значит, к моменту обработки маски все маски, из которых в неё можно попасть, уже обработаны. Топологический порядок получается сам, без всякого топсорта, — и это общая причина, почему динамика по маскам пишется простым for.

Пример: когда позиция не нужна

Есть nn встреч. На встречу ii можно прийти, только если текущее настроение лежит в [ai,bi][a_i, b_i]; после встречи настроение меняется на +ci+c_i. Начальное настроение kk. Максимум встреч.

Здесь номер последней встречи в состояние не нужен. Настроение после набора встреч равно k+imaskcik + \sum_{i \in \text{mask}} c_i и от порядка не зависит.

Поэтому dp[mask]dp[\text{mask}] — просто «достижима ли такая маска», а ответ — маска с наибольшим числом единиц. Состояний 2n2^n вместо 2nn2^n n.

Вывод общий: прежде чем добавлять параметр в состояние, проверьте, влияет ли он на будущее. Здесь не влиял.

Функция от маски за суммарную линию

Считать imaskci\sum_{i \in \text{mask}} c_i перебором битов — это O(2nn)O(2^n n) на всё. Можно за O(2n)O(2^n).

Возьмём любой установленный бит маски и сведём к маске без него:

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 наборов совпало с прямым подсчётом.

В приведённой задаче эта оптимизация не нужна — там доминирует другой цикл. Но встречаются задачи, где подсчёт функции по маскам и есть основная работа.

Типовые применения

задача состояние
гамильтонов путь, задача коммивояжёра маска + последняя вершина
разбиение множества на группы маска обработанных
паросочетание в двудольном графе с малой долей маска занятых справа + номер слева
раскраска графа в kk цветов маска покрашенных
покрытие множества подмножествами маска покрытых

Во всех случаях ограничение читается прямо из условия: если в задаче написано n20n \le 20 и речь про подмножества — это маски.