[Перевод] Быстрая свёртка множеств (алгоритм)
Эту статью меня вдохновила написать задача с codeforces. В статье будет разобран алгоритм для решения задачи.
Даны ,
(пояснение) нужно найти
такую что:
За время где
Но для решения нам нужно будет построить небольшую математическую теорию))
Вводная задача
Дано множество размера
и функция
. Нужно для
найти:
Дальше для обозначения множество будем использовать int mask
в его битном представление. Например у нас множество состоит из 20 элементов, то тогда первые 20 бит в mask
будут обозначать есть ли этот элемент в множестве или нет в зависимости от 0 и 1.
Пример: mask = 0010010
это значит что состоит из 2 и 5 элементов из
.
Тривиальный алгоритм
for (int mask = 0; mask < (1 << N); ++ mask) {
hat_f[mask] = 0;
for (int sub = 0; sub < (1 << N); ++ sub) {
// проверяем является ли mask2 подмножеством mask1
if (sub | mask == mask) {
hat_f[mask] += f[sub];
}
}
}
Тут мы для каждого множества перебираем остальные множества и если оно оказалось подмножеством, то добавляем функцию к результату.
Работает за
Простой алгоритм
for (int mask = 0; mask < (1 << N); ++ mask) {
hat_f[mask] = f[0];
// сразу перебираем все подмножества mask1
for (int sub = mask; sub > 0; (sub - 1) & mask) {
hat_f[mask] += f[sub];
}
}
Тут мы сразу перебираем все подмножества, без проверки лишних.
Асимптотика
Быстрый алгоритм
Давайте введем вспомогательную функцию:
Мы смотрим на подмножества которые отличаются от только в первых
битах.
Пример: тут мы зафиксировали последние 3 бита и перебираем подмножества по первым 5 битам.
Теперь заметим одно замечательное свойство:

И тогда, получаем такую динамику:
for(int mask = 0; mask < (1 << N); ++ mask) {
dp[mask][0] = f[mask];
for (int i = 1; i <= N; ++ i) {
if (mask & (1 << i - 1)) {
dp[mask][i] = dp[mask][i - 1] + dp[mask^(1 << i - 1)][i - 1];
} else {
dp[mask][i] = dp[mask][i - 1];
}
}
hat_f[mask] = dp[mask][N];
}
Можно написать более просто:
for(int mask = 0; mask < (1 << N); ++ mask)
hat_f[mask] = f[mask];
for (int i = 0; i < N; ++ i)
for (int mask = 0; mask < (1 << N); ++ mask)
if (mask & (1 << i))
hat_f[mask] += hat_f[mask ^ (1 << i)];
Асимптотика
Свертка множеств
Наша полученная функция называется сверткой (Zeta Transform) и обозначается
. Но для того чтобы она была полноценной, нужна и развертка)
Рассмотрим трансформацию Мёбиуса (Mobius Transform):
Пусть .
Тогда
Доказательство:
// сразу заполняем с действием sigma
for (uint mask = 0; mask < (1 << N); ++ mask) {
hat_f[mask] = ((popcount(mask) & 1) ? -1 : 1) * f[mask];
}
// z свертка
for (int i = 0; i < N; ++ i)
for (int mask = 0; mask < (1 << N); ++ mask)
if (mask & (1 << i))
hat_f[mask] += hat_f[mask ^ (1 << i)];
// sigma преобразование
for (uint mask = 0; mask < (1 << N); ++ mask) {
hat_f[mask] *= (popcount(mask) & 1) ? -1 : 1;
}
popcount()
считает количество 1ных бит в числе для с++20.
__builtin_popcount()
тоже самое, но для более ранних версий с++.
Для неё выполняется, что как раз и является обратным преобразованием : D
Тогда где
это почленное умножение, это тоже самое, что:
А если мы воспользуемся нашей сверткой :
где побитовое
, что эквивалентно
Возвращаемся к основной задаче
Но всё ещё нет решения на изначальную задачу. Предыдущая формула не подходит, так как для может быть, что
, а нам нужно чтобы
и
не пересекались.
Но есть решение!
1) Введем новую функцию
И для ведем аналогичную
.
2) Пусть сумма в смысле почленного суммирования.
3) Тогда .
Это и будет решение нашей задачи потому, что:
1) Предположим что:
2) Тогда:
3) Пусть , тогда:
4) Выводим что:
Значит действительно подходит
5) и последний шаг из пункта 2.
Что и требовалось доказать.
Пишем код
// заполняем f_i и g_i
for (uint mask = 0; mask < (1 << N); ++ mask) {
hat_f[popcount(mask)][mask] = f[mask];
hat_g[popcount(mask)][mask] = g[mask];
}
// применяем z свертку к f_i и g_i
for (int i = 0; i < N; ++ i)
for (int j = 0; j < N; ++ j)
for (int mask = 0; mask < (1 << N); ++ mask)
if (mask & (1 << j)){
hat_f[i][mask] += hat_f[i][mask ^ (1 << j)];
hat_g[i][mask] += hat_g[i][mask ^ (1 << j)];
}
// делаем внутрение преобразование
for (int i = 0; i < N; ++ i)
for (int j = 0; j <= i; ++ j)
for(int mask = 0; mask < (1 << N); ++ mask)
hat_mu[i][mask] += hat_f[j][mask] * hat_g[i - j][mask];
// применяем преобразование Мёбиуса hat_mu
// sigma преобразование
for (int i = 0; i < N; ++ i)
for(uint mask = 0; mask < (1 << N); ++ mask)
hat_mu[i][mask] *= (popcount(mask) & 1) ? -1 : 1;
// z cсвертка
for (int i = 0; i < N; ++ i)
for (int j = 0; j < N; ++ j)
for(int mask = 0; mask < (1 << N); ++ mask)
if (mask & (1 << j))
hat_mu[i][mask] += hat_mu[i][mask ^ (1 << j)];
// sigma преобразование
for (int i = 0; i < N; ++ i)
for(uint mask = 0; mask < (1 << N); ++ mask)
hat_mu[i][mask] *= (popcount(mask) & 1) ? -1 : 1;
// Запись в ответ
for(uint mask = 0; mask < (1 << N); ++ mask)
mu[mask] = hat_mu[popcount(mask)][mask];
popcount()
считает количество 1ных бит в числе для с++20.
__builtin_popcount()
тоже самое, но для более ранних версий с++.
Применение
Этот алгоритм очень полезен для работы с множествами. Ну и конечно в олимпиадном программирование иногда (очень редко) встречается)))
Ссылки
Источник для основного алгоритма
Источник для первой задачи
Статья с другими свертками множеств
Быстрое преобразование Фурье на википедии и алгоритмике
Спасибо за то что прочитали, всем хорошего настроения!