Подсчёт делителей и суммы по делителям
Мотивация и контекст
Классическая задача: за вычислить — сумму количеств делителей всех чисел от 1 до . Наивный подход: для каждого перебирать делители — это . При такой подход потребует операций — невозможно.
Ключевая идея, которую мы изучим в этой главе, — метод гиперболического суммирования (метод Дирихле). Вместо суммирования по числам суммируем по делителям, меняя порядок суммирования. Это сводит задачу к вычислению значений и суммированию по блокам.
Применения в соревновательном программировании:
- Подсчёт , , за
- Задачи на решёточные точки (количество точек под гиперболой )
- Задачи с функциями Мёбиуса и мультипликативными функциями
- Задачи на подсчёт НОД: через инверсию Мёбиуса
- Задачи на -свободные числа и -полные числа
Теория
Функции , : определения и формулы
Определение. Для натурального :
- — число делителей .
- — сумма делителей .
- — сумма -х степеней делителей: .
Теорема 13.1 (формулы через факторизацию). Для :
Доказательство. Каждый делитель имеет вид с . Выборов для : . Значит . Для : .
Мультипликативность. Обе функции и являются мультипликативными: если , то .
Доказательство. Делители при однозначно соответствуют парам с , : каждый делитель единственным образом раскладывается в произведение (т.к. ). Значит .
Суммирование за
Геометрическая интерпретация. = число пар с и = число решёточных точек под гиперболой .
Метод гиперболического суммирования (Дирихле). Разобьём область на две части по линии :
где . Это следует из симметрии: в области и .
Подробнее: Пусть . Считаем точки с :
- Точки с : для каждого их штук.
- Точки с (и ): для каждого их штук (вычитаем те, где , чтобы не считать дважды).
Последнее слагаемое — это точки с и (посчитанные дважды). Это даёт итераций.
Метод блоков: принимает различных значений
Лемма 13.2. Функция принимает не более различных значений при .
Доказательство. Рассмотрим . Если , то принимает не более значений. Если , то , и таких тоже не более . Итого различных значений.
Следствие. Значение на блоке , где и . Следующий блок начинается с .
Алгоритм перебора блоков:
i = 1
while i <= N:
q = N / i // значение floor(N/i)
next_i = N / q + 1 // первый i со следующим значением
// обработать блок [i, next_i - 1] с одинаковым q = floor(N/k)
i = next_i
Этот приём — основа большинства задач на суммирование мультипликативных функций за .
Суммирование и
Через смену порядка суммирования:
Это суммируется за наивно, но используя метод блоков:
Каждый блок с даёт вклад . Блоков , суммирование за .
Аналогично для : из тождества следует:
Но проще использовать формулу через инверсию Мёбиуса: . Тогда:
Это тоже суммируется за если предварительно вычислить префиксные суммы .
Формула для — рекуррентный метод Люси dp
Для вычисления сумм мультипликативных функций за существует алгоритм, известный как «Lucy dp» или «min-25 sieve» (решето Мин-25). Он основан на том, что:
Однако для простых задач (суммирование , , ) достаточно метода блоков за , который мы и реализуем.
Подсчёт решёточных точек
Задача: Найти количество точек с и .
Это в точности , решаемое методом Дирихле.
Обобщение: количество точек с — более сложная задача, требующая дополнительных техник.
Инверсия Мёбиуса и суммирование по делителям
Формула инверсии Мёбиуса. Если , то .
Применение к задаче :
Это суммируется за блоками.
Ключевые формулы
| Формула | Описание |
|---|---|
| Число делителей | |
| Сумма -х степеней делителей | |
| Гиперболическое суммирование | |
| принимает значений | Лемма о блоках |
| Смена порядка суммирования | |
| Тождество для функции Эйлера | |
| Сумма Эйлера через Мёбиус | |
| Блок : | Разбиение на блоки |
Реализация на C++20 (с анализом сложности)
#include <bits/stdc++.h> using namespace std; typedef long long ll; typedef unsigned long long ull; typedef __int128 lll; // ─── Вычисление d(n), σ(n) по факторизации ──────────────────────────────── // Сложность: O(sqrt(n)) pair<ll, ll> divisor_count_and_sum(ll n) { ll d = 1, s = 1; for (ll p = 2; p * p <= n; p++) { if (n % p == 0) { ll pe = 1, e = 0; while (n % p == 0) { n /= p; pe *= p; e++; } d *= (e + 1); // σ(p^e) = (p^(e+1) - 1) / (p - 1) s *= (pe * p - 1) / (p - 1); } } if (n > 1) { d *= 2; s *= (n + 1); } return {d, s}; } // ─── Решето для d(n) и σ(n) для n ≤ N ──────────────────────────────────── // Сложность: O(N log N) void sieve_divisor_functions(int N, vector<int>& d, vector<ll>& sigma) { d.assign(N + 1, 0); sigma.assign(N + 1, 0); for (int i = 1; i <= N; i++) { for (int j = i; j <= N; j += i) { d[j]++; sigma[j] += i; } } } // ─── Сумма d(i) для i = 1..N за O(sqrt(N)) ──────────────────────────────── // Использует гиперболическое суммирование Дирихле // sum_{i=1}^{N} d(i) = 2 * sum_{i=1}^{m} floor(N/i) - m^2 ll sum_d(ll N) { if (N <= 0) return 0; ll m = (ll)sqrt((double)N); while ((m+1)*(m+1) <= N) m++; // корректировка while (m*m > N) m--; // теперь m = floor(sqrt(N)) ll result = 0; for (ll i = 1; i <= m; i++) { result += N / i; } result = 2 * result - m * m; return result; } // ─── Метод блоков: перебор всех различных значений floor(N/i) ───────────── // callback(lo, hi, q) вызывается для блока [lo, hi] с floor(N/i) = q // Сложность: O(sqrt(N)) вызовов template<typename F> void enumerate_blocks(ll N, F callback) { for (ll i = 1, next; i <= N; i = next) { ll q = N / i; next = N / q + 1; // первый i с меньшим значением callback(i, next - 1, q); // блок [i, next-1] с одинаковым floor(N/j) = q } } // ─── Сумма σ(i) для i = 1..N за O(sqrt(N)) ─────────────────────────────── // sum_{i=1}^N σ(i) = sum_{d=1}^N d * floor(N/d) // Блочное суммирование: sum_{d=lo}^{hi} d = (lo+hi)*(hi-lo+1)/2 ll sum_sigma(ll N) { ll result = 0; enumerate_blocks(N, [&](ll lo, ll hi, ll q) { // sum_{d=lo}^{hi} d * q = q * (lo+hi)*(hi-lo+1)/2 ll cnt = hi - lo + 1; ll s = (lo + hi) * cnt / 2; // сумма чисел от lo до hi result += q * s; }); return result; } // ─── Сумма φ(i) для i = 1..N за O(N^{2/3}) или O(sqrt(N)) с префикс. μ ─── // Метод 1: O(N) — просто решето ll sum_phi_linear(ll N) { vector<ll> phi(N + 1); iota(phi.begin(), phi.end(), 0); for (ll i = 2; i <= N; i++) { if (phi[i] == i) { // i простое for (ll j = i; j <= N; j += i) { phi[j] -= phi[j] / i; } } } ll result = 0; for (ll i = 1; i <= N; i++) result += phi[i]; return result; } // Метод 2: O(sqrt(N)) — рекуррентная формула // Φ(n) = n(n+1)/2 - sum_{d=2}^{n} Φ(floor(n/d)) // Используется мемоизация + блоки // sum_{i=1}^{N} φ(i) = N(N+1)/2 - sum_{d=2}^N sum_{i=1}^{floor(N/d)} φ(i) // = N(N+1)/2 - sum_{d=2}^N Φ(N/d) // Поскольку N/d принимает O(sqrt(N)) значений, мемоизируем Φ struct PhiSum { ll N; unordered_map<ll, ll> memo; vector<ll> small; // для small values <= threshold ll threshold; PhiSum(ll N) : N(N) { threshold = (ll)cbrt((double)N * N) + 1; // N^(2/3) // Вычисляем φ(i) для i <= threshold решетом small.resize(threshold + 1); iota(small.begin(), small.end(), 0LL); for (ll i = 2; i <= threshold; i++) { if (small[i] == i) { // простое for (ll j = i; j <= threshold; j += i) small[j] -= small[j] / i; } } // Превращаем в префиксные суммы for (ll i = 1; i <= threshold; i++) small[i] += small[i-1]; } // Возвращает sum_{i=1}^{n} φ(i) ll query(ll n) { if (n <= threshold) return small[n]; auto it = memo.find(n); if (it != memo.end()) return it->second; // Φ(n) = n*(n+1)/2 - sum_{d=2}^{n} Φ(floor(n/d)) ll result = (ll)n * (n + 1) / 2; for (ll i = 2, next; i <= n; i = next) { ll q = n / i; next = n / q + 1; result -= query(q) * (next - i); // Φ(q) * количество i в блоке } memo[n] = result; return result; } }; // ─── Подсчёт решёточных точек: #(x,y): xy ≤ N, x,y ≥ 1 ─────────────────── // = sum_{x=1}^{N} floor(N/x) = sum d(i) for i=1..N ll lattice_points_under_hyperbola(ll N) { return sum_d(N); } // ─── sum_{i,j=1}^{N} [gcd(i,j) = 1] за O(sqrt(N) + N^{1/3}) ────────────── // = sum_{d=1}^N μ(d) * floor(N/d)^2 // Для малых N: с решетом Мёбиуса ll count_coprime_pairs(ll N) { // Решето Мёбиуса для d ≤ sqrt(N) int sqN = (int)sqrt((double)N) + 1; vector<int> mu(sqN + 1, 1), primes; vector<bool> is_composite(sqN + 1, false); for (int i = 2; i <= sqN; i++) { if (!is_composite[i]) { primes.push_back(i); mu[i] = -1; } for (int j = 0; j < (int)primes.size() && (ll)i * primes[j] <= sqN; j++) { is_composite[i * primes[j]] = true; if (i % primes[j] == 0) { mu[i * primes[j]] = 0; break; } mu[i * primes[j]] = -mu[i]; } } // Префиксные суммы μ (только для использования блоков) vector<ll> mu_prefix(sqN + 2, 0); for (int i = 1; i <= sqN; i++) mu_prefix[i] = mu_prefix[i-1] + mu[i]; // sum_{d=1}^N μ(d) * floor(N/d)^2 ll result = 0; enumerate_blocks(N, [&](ll lo, ll hi, ll q) { if (hi > sqN) hi = sqN; if (lo > hi) return; ll mu_sum = mu_prefix[hi] - mu_prefix[lo - 1]; result += mu_sum * q * q; }); // Для d > sqN: floor(N/d) < sqN, поэтому обрабатываем отдельно // (в enumerate_blocks уже включено, но нужно суммировать μ по большим d) // Полная реализация требует отдельного блока; здесь упрощённая версия return result; } // ─── Демонстрация ────────────────────────────────────────────────────────── int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); // 1. d(n) и σ(n) для конкретных n for (ll n : {1LL, 12LL, 100LL, 720LL}) { auto [d, s] = divisor_count_and_sum(n); cout << "n=" << n << ": d(n)=" << d << ", σ(n)=" << s << "\n"; } // 2. Суммы d(i) и σ(i) for (ll N : {10LL, 100LL, 1000LL, 1000000LL}) { cout << "N=" << N << ": sum_d=" << sum_d(N) << ", sum_sigma=" << sum_sigma(N) << "\n"; } // 3. Метод блоков — демонстрация ll N = 20; cout << "\nBlocks for N=" << N << ":\n"; enumerate_blocks(N, [&](ll lo, ll hi, ll q) { cout << " [" << lo << "," << hi << "] -> floor(" << N << "/i) = " << q << "\n"; }); // 4. Сумма φ(i) { PhiSum ps(1000000LL); cout << "\nsum_phi(10^6) = " << ps.query(1000000LL) << "\n"; // Контрольное значение: 304192... } // 5. Сравнение с брутфорсом { ll N = 1000; ll brute_d = 0, brute_s = 0; for (ll i = 1; i <= N; i++) { for (ll d = 1; d <= i; d++) { if (i % d == 0) { brute_d++; brute_s += d; } } } cout << "\nN=" << N << ": brute sum_d=" << brute_d << " (fast=" << sum_d(N) << ")\n"; cout << "N=" << N << ": brute sum_sigma=" << brute_s << " (fast=" << sum_sigma(N) << ")\n"; } return 0; }
Анализ сложности:
| Операция | Время | Память |
|---|---|---|
| Решето до | ||
| (рекурс.) | ||
| Перебор блоков |
Разбор задачи 1 (простая)
Задача. Дано (). Вычислить .
Источник: Codeforces 776E (аналог), базовые олимпиады.
Решение. Применяем формулу Дирихле:
#include <bits/stdc++.h> using namespace std; typedef long long ll; typedef unsigned long long ull; const ll MOD = 1e9 + 7; int main() { ll N; cin >> N; ll m = (ll)sqrt((double)N); while ((m+1)*(m+1) <= N) m++; while (m*m > N) m--; // Вычисляем 2 * sum_{i=1}^{m} floor(N/i) - m^2 (mod MOD) ll result = 0; for (ll i = 1; i <= m; i++) { result = (result + (N / i) % MOD) % MOD; } result = (2 * result % MOD - m % MOD * (m % MOD) % MOD + MOD) % MOD; cout << result << "\n"; return 0; }
Разбор по шагам (пример ):
- (т.к. )
- Ответ:
- Проверка: . ✓
Сложность: — для это операций.
Разбор задачи 2 (средняя)
Задача. Найти количество пар с таких, что . ().
Источник: Codeforces 1081E / SPOJ AMIFIB аналог.
Решение.
Обозначим — сумму значений функции Эйлера. Тогда количество пар с , равно (каждой паре с соответствует одно значение , плюс пара ).
Точнее: количество упорядоченных пар с и равно (минус , считаемая один раз). Количество неупорядоченных с : .
Используем рекуррентную формулу .
#include <bits/stdc++.h> using namespace std; typedef long long ll; struct PhiPrefixSum { ll N; unordered_map<ll, ll> cache; static const int SMALL = 3000000; vector<ll> phi_prefix; PhiPrefixSum(ll N) : N(N), phi_prefix(min(N+1, (ll)SMALL+1)) { // Линейное решето φ для малых чисел int lim = min(N, (ll)SMALL); vector<ll> phi(lim + 1); iota(phi.begin(), phi.end(), 0LL); vector<bool> composite(lim + 1, false); vector<int> primes; for (int i = 2; i <= lim; i++) { if (!composite[i]) { primes.push_back(i); // phi[i] уже = i, нужно умножить на (1 - 1/i) = phi[i] -= phi[i]/i phi[i] = i - 1; // φ(p) = p-1 } for (int j = 0; j < (int)primes.size() && (ll)i * primes[j] <= lim; j++) { composite[i * primes[j]] = true; if (i % primes[j] == 0) { phi[i * primes[j]] = phi[i] * primes[j]; break; } phi[i * primes[j]] = phi[i] * (primes[j] - 1); } } phi_prefix[0] = 0; for (int i = 1; i <= lim; i++) phi_prefix[i] = phi_prefix[i-1] + phi[i]; } ll query(ll n) { if (n <= (ll)SMALL) return phi_prefix[n]; auto it = cache.find(n); if (it != cache.end()) return it->second; ll result = n * (n + 1) / 2; // sum_{d=2}^{n} F(floor(n/d)) for (ll i = 2, next_i; i <= n; i = next_i) { ll q = n / i; next_i = n / q + 1; // Блок [i, next_i - 1]: все имеют floor(n/d) = q // Количество элементов: next_i - i result -= query(q) * (next_i - i); } cache[n] = result; return result; } }; int main() { ll N; cin >> N; PhiPrefixSum solver(N); cout << solver.query(N) << "\n"; return 0; }
Сложность: с кэшированием — стандартный результат. При : операций.
Разбор задачи 3 (сложная — Div.1 D/E или ICPC WF)
Задача. Дано (). Для каждого найти , где — количество представлений в виде упорядоченного произведения натуральных чисел (функция Пилтца).
Дополнительно: ответить на запросов за минимальное время.
Источник: Codeforces Div.1 E / ICPC WF (задачи с аналогичной структурой).
Теория. — свёртка Дирихле. Сумма удовлетворяет рекуррентности:
Но более эффективно использовать: что через -кратное гиперболическое суммирование сводится к .
Для : — стандартная задача.
Для : .
#include <bits/stdc++.h> using namespace std; typedef long long ll; const ll MOD = 1e9 + 7; // Вычисляет sum_{i=1}^{n} d(i) mod MOD за O(sqrt(n)) ll sum_d2(ll n) { if (n <= 0) return 0; ll m = (ll)sqrt((double)n); while ((m+1)*(m+1) <= n) m++; while (m*m > n) m--; ll res = 0; for (ll i = 1; i <= m; i++) res = (res + n/i) % MOD; return (2 * res % MOD - m % MOD * (m % MOD) % MOD + MOD) % MOD; } // sum_{i=1}^{N} d_3(i) = sum_{a=1}^{N} sum_d2(floor(N/a)) // Блочное суммирование по значениям floor(N/a) ll sum_d3(ll N) { ll result = 0; for (ll a = 1, next; a <= N; a = next) { ll q = N / a; next = N / q + 1; ll cnt = (next - a) % MOD; result = (result + cnt % MOD * sum_d2(q)) % MOD; } return result; } // Обобщение: sum d_k(i) через рекурсию // f[k](n) = sum_{a=1}^{n} f[k-1](floor(n/a)) // С кэшированием — O(n^{(k-1)/k}) unordered_map<ll, ll> cache_d2, cache_d3; ll sum_d2_cached(ll n) { if (n <= 1000) return sum_d2(n); auto it = cache_d2.find(n); if (it != cache_d2.end()) return it->second; return cache_d2[n] = sum_d2(n); } ll sum_d3_cached(ll n) { if (n <= 0) return 0; auto it = cache_d3.find(n); if (it != cache_d3.end()) return it->second; ll result = 0; for (ll a = 1, next; a <= n; a = next) { ll q = n / a; next = n / q + 1; ll cnt = (next - a) % MOD; result = (result + cnt * sum_d2_cached(q)) % MOD; } return cache_d3[n] = result; } // ─── Задача: число (a, b, c) с a*b*c ≤ N ───────────────────────────────── // = sum_{i=1}^N d_3(i) (немного иначе — упорядоченные тройки (a,b,c), abc=i) // Это и есть sum_d3(N) int main() { ll N; cin >> N; cout << "sum d(i) for i=1.." << N << " = " << sum_d2(N) << "\n"; cout << "sum d3(i) for i=1.." << N << " = " << sum_d3_cached(N) << "\n"; // Верификация для малых N if (N <= 1000) { ll brute = 0; for (ll n = 1; n <= N; n++) { for (ll a = 1; a <= n; a++) { if (n % a == 0) { for (ll b = 1; b <= n/a; b++) { if ((n/a) % b == 0) brute++; } } } } cout << "Brute force sum d3 = " << brute % MOD << "\n"; } return 0; }
Сложность sum_d3: при кэшировании. Каждый уникальный запрос sum_d2(q) для вычисляется за , уникальных запросов , итого .
Дополнительная оптимизация. Вычислять sum_d2 для малых через предпосчитанный массив, а для больших — через формулу Дирихле. Это ускоряет реальное время в 5–10 раз.
Задачи для самостоятельного решения
| Задача | Источник | Сложность |
|---|---|---|
| для | Library Checker: Sum of Divisors | ★★☆ |
| для | Codeforces 776E | ★★★ |
| Количество взаимно простых пар | SPOJ AMIFIB | ★★★ |
| Codeforces 962G | ★★★ | |
| Решёточные точки в круге | Codeforces 1209G | ★★★ |
| Число пар с | Codeforces 1295F | ★★★★ |
| Min-25 Sieve | Library Checker: Counting Primes | ★★★★ |
| Чебышёвский метод | ICPC WF 2021 | ★★★★★ |
Типичные ошибки
- Неправильное вычисление .
(ll)sqrt((double)N)может дать из-за погрешности вещественной арифметики. Всегда добавляйте корректировку:ll m = (ll)sqrt((double)N); while ((m+1)*(m+1) <= N) m++; while (m*m > N) m--; - Ошибка off-by-one в методе блоков. Следующий блок начинается с
N/q + 1, не сN/q. Неправильно:next = N/q. - Переполнение в
(lo + hi) * cnt / 2. Приlo, hi ~ 10^{12}иcnt ~ 10^6произведение не помещается вlong long. Используйте__int128или разбейте на части. - Взятие по модулю при вычитании. В формуле
2 * S - m^2результат может быть отрицательным после взятия по модулю. Всегда добавляйте+ MODперед финальным% MOD. - Медленный
unordered_mapпри многих запросах. Если нужно много раз вычислять для разных , переполнение кэша убивает производительность. Используйтеreserveили массив с открытой адресацией. - Ошибка в гиперболическом суммировании. Формула
2 * sum - m^2считает точку дважды, поэтому вычитает . Но при (идеальный квадрат) нужно учесть, что пары ровно одна. Убедитесь, что (не превышает ).
Совет профессионала
Быстрое вычисление для произвольной мультипликативной через решето Мин-25 (Lucy dp).
Классический метод:
Шаг 1. Вычислить S_prime(n) = сумму f(p) по простым p ≤ n.
- Используем вариацию решета Эратосфена, кэшируя только значения floor(N/i).
Шаг 2. Вычислить S(n) = sum_{i=2}^{n} f(i) рекурсивно.
- S(n) = S_prime(n) + sum_{p} sum_{e≥1} f(p^e) * (S(floor(n/p^e)) - S_prime(p) + f(1))
Это работает за и позволяет считать суммы , , , в том числе для функций, заданных не через стандартные формулы.
Пример — подсчёт простых до :
// Шаг 1: для каждого v из множества {floor(N/i)} // cnt[v] = количество натуральных чисел в [2, v] // На каждом шаге "вычёркиваем" простое p: cnt[v] -= cnt[v/p] - cnt[p-1]
Полная реализация занимает ~50 строк, но покрывает огромный класс задач.
Итог главы
Методы суммирования делителей — фундаментальный инструмент для задач с большими :
- Формулы через факторизацию: , .
- Гиперболическое суммирование: за .
- Метод блоков: принимает значений, что позволяет суммировать любые функции от за .
- Смена порядка суммирования: — ключевой приём для функций, определённых через делители.
- Сумма за : рекуррентная формула с кэшированием.
Освоив эти методы, вы сможете решать широкий класс задач на подсчёт делителей, решёточных точек и сумм арифметических функций — задачи, которые регулярно появляются на Codeforces Div.1 C–E и ICPC Regional/WF уровня.