Эту статью меня вдохновила написать задача с 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()
тоже самое, но для более ранних версий с++.
Для неё выполняется, что как раз и является обратным преобразованием
Доказательство
Это очень напоминает FFT. Пусть у нас даны и
Тогда где
это почленное умножение, это тоже самое, что:
А если мы воспользуемся нашей сверткой :
где побитовое
, что эквивалентно
Доказательство
Подробнее про суммы с операциями ,
и
можно прочитать на этом сайте.
Возвращаемся к основной задаче
Но всё ещё нет решения на изначальную задачу. Предыдущая формула не подходит, так как для может быть, что
, а нам нужно чтобы
и
не пересекались.
Но есть решение!
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()
тоже самое, но для более ранних версий с++.
За время где
Применение
Этот алгоритм очень полезен для работы с множествами. Ну и конечно в олимпиадном программирование иногда (очень редко) встречается)))
Ссылки
Источник для основного алгоритма
Статья с другими свертками множеств
Быстрое преобразование Фурье на википедии и алгоритмике
Спасибо за то что прочитали, всем хорошего настроения!
ссылка на оригинал статьи https://habr.com/ru/articles/891188/
Добавить комментарий