当前位置:首页 > 技术 > 正文内容

Min_25筛法:一种高效的数论求和算法

访客 技术 2026年6月22日 1

前置数学符号 在展开讨论之前,先约定本文所使用的符号体系:

  • lpf(x) 为整数 x 的最小质因子
  • p_k 为第 k 个质数
  • π(n) 为不超过 n 的质数总数

算法核心思想

Min_25筛法是一种用于计算数论函数前缀和的巧妙算法。其基本思路是将求和过程拆解为质数部分与合数部分分别处理。设我们要求解的是:

∑i=1n [i ∈ ℙ] f(i)

这里的 ℙ 表示质数集合。由于 n 的规模通常很大,直接枚举所有质数显然不切实际。Min_25筛采用了一种"拟合"策略:构造一个完全积性函数 F,使得 F(p) = f(p) 对所有质数 p 成立。

有了完全积性函数 F 后,原问题转化为:

∑i=1n [i ∈ ℙ] F(i)

容斥原理的应用

直接计算质数处的函数值很困难,我们转而采用容斥思想:用 ∑<sub>i=1</sub><sup>n</sup> F(i) 减去所有合数处的 F 值。注意到任意合数 x 都满足 lpf(x) ≤ ⌊√x⌋,这意味着我们可以采用类似埃拉托斯特尼筛法的思路,遍历所有不超过 ⌊√n⌋ 的质数,逐步扣除它们的贡献。

动态规划 formulation

定义状态:

g(n, x) = ∑i=1n [i ∈ ℙ 或 lpf(i) > px] F(i)

即统计在 n 范围内,所有质数以及最小质因子大于 p_x 的数对应的 F 值之和。

考虑从 g(n, x-1) 转移到 g(n, x)。当限制条件从 lpf > p<sub>x-1</sub> 变为 lpf > p_x 时,被剔除的恰好是那些最小质因子恰好等于 p_x 的数。因此:

Δ = g(n, x-1) - g(n, x) = ∑i=1n [lpf(i) = p_x] F(i)

接下来分析 lpf(i) = p_x 的结构。由于 F 是完全积性函数,我们可以提取一个因子 F(p_x),剩下的部分需要满足 lpf ≥ p_x。考察 g(⌊n/p_x⌋, x-1),发现它比目标多计算了那些质因子小于 p_x 的质数处的 F 值。预先处理 sp_x = ∑<sub>i=1</sub><sup>x</sup> F(p_i),即可得到递推公式:

g(n, x) = g(n, x-1) - F(p_x) · (g(⌊n/p_x⌋, x-1) - spx-1)

根据杜教筛的理论,我们仅需要 g(⌊n/i⌋, x) 中满足 p_x² ≤ ⌊n/i⌋ 的那些状态。可以证明,对这些关键点进行动态规划,时间复杂度为 O(n^{3/4} / log n)

离散化处理

实现时需要对 ⌊n/i⌋ 的不同取值进行编号。采用两个大小为 O(√n) 的数组 idx_smallidx_large:当 ⌊n/i⌋ ≤ √n 时,将编号存入 idx_small[⌊n/i⌋];否则令 j = ⌊n / ⌊n/i⌋⌋,将编号存入 idx_large[j] 中。

最终我们只需要 g(n, π(⌊√n⌋)),即所有质数处的 F 值之和。由于第二维只与其前一个状态有关,可以采用滚动数组优化空间。

处理原始函数 f

定义另一状态:

S(n, x) = ∑i=1n [lpf(i) > p_x] f(i)

将求和分为质数和合数两部分考虑。

质数部分的贡献

对于质数部分,贡献为:

g(n, π(⌊√n⌋)) - sp_x

合数部分的贡献

由于 f 不一定是完全积性函数,无法单独提取质因子。这里采用枚举质数和指数的策略:先枚举 p_i (i > x),再枚举其指数 c,这样可以将 f(p_i^c) 完整提取出来。剩余部分应满足 lpf > p_i,即 S(⌊n/p_i^c⌋, i)。需要注意的是,当 c > 1 时,需要补回 f(p_i^c);当 c = 1 时,f(p_i) 已在质数部分计算过,无需重复。

综合得到递推式:

S(n, x) = g(n, π(⌊√n⌋)) - sp_x + ∑i=x+1p_i²≤n ∑c≥1, p_i^c≤n f(p_i^c) · (S(⌊n/p_i^c⌋, i) + [c > 1])

该部分的时间复杂度被证明是 O(n^{1-ε})

最终答案为 S(n, 0) + f(1)

算法要求

Min_25筛法对目标函数 f 有以下要求:

  • 能够快速计算 F(p) = f(p),且 F 的前缀和可快速求得
  • 能够快速计算 f(p^c)c ≥ 1

这些条件在实际应用中通常容易满足。若 f(p) 可以分解为若干完全积性函数的线性组合,则对应地组合 gsp 的值即可。

应用实例

示例一:Luogu P5325 本题要求 f(p) = p² - p。构造 F₁(p) = pF₂(p) = p² 即可求解。

#include <bits/stdc++.h>
using namespace std;
using int64 = long long;
const int MAXN = 2000005;
const int MOD = 1e9 + 7;

int64 n, sqrt_n;
int64 values[MAXN << 1];
int prime_cnt;
int primes[MAXN / 10];
int64 sum_p1[MAXN / 10];
int64 sum_p2[MAXN / 10];
int total_vals;
int idx_small[MAXN];
int idx_large[MAXN];
int64 dp1[MAXN << 1];
int64 dp2[MAXN << 1];
bool is_composite[MAXN];

int64 addmod(int64 a, int64 b) {
    a += b;
    if (a >= MOD) a -= MOD;
    return a;
}
int64 submod(int64 a, int64 b) {
    a -= b;
    if (a < 0) a += MOD;
    return a;
}
int64 mulmod(int64 a, int64 b) {
    return (a * b) % MOD;
}
int64 mod_pow(int64 a, int64 e) {
    int64 r = 1;
    while (e) {
        if (e & 1) r = mulmod(r, a);
        a = mulmod(a, a);
        e >>= 1;
    }
    return r;
}

void init_sieve(int limit) {
    for (int i = 2; i <= limit; ++i) {
        if (!is_composite[i]) {
            primes[++prime_cnt] = i;
            sum_p1[prime_cnt] = addmod(sum_p1[prime_cnt - 1], i);
            sum_p2[prime_cnt] = addmod(sum_p2[prime_cnt - 1], mulmod(i, i));
        }
        for (int j = 1; j <= prime_cnt && i * primes[j] <= limit; ++j) {
            is_composite[i * primes[j]] = true;
            if (i % primes[j] == 0) break;
        }
    }
}

int64 compute_prefix_sum(int64 x) {
    return mulmod(mulmod(x % MOD, (x + 1) % MOD), 500000004);
}

int64 compute_square_sum(int64 x) {
    x %= MOD;
    return mulmod(mulmod(x, (x + 1) % MOD), mulmod((2 * x + 1) % MOD, 166666668));
}

int64 solve_recursive(int64 m, int pos) {
    if (primes[pos] >= m) return 0;
    int index = (m <= sqrt_n) ? idx_small[m] : idx_large[n / m];
    int64 result = submod(submod(dp2[index], dp1[index]), 
                          submod(sum_p2[pos], sum_p1[pos]));
    
    for (int i = pos + 1; i <= prime_cnt && 1LL * primes[i] * primes[i] <= m; ++i) {
        int64 p = primes[i];
        for (int64 power = p, exp = 1; power <= m; power *= p, ++exp) {
            int64 p_mod = power % MOD;
            int64 term = mulmod(mulmod(p_mod, submod(p_mod, 1)), 
                               addmod(solve_recursive(m / power, i), exp > 1));
            result = addmod(result, term);
        }
    }
    return result;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    cin >> n;
    sqrt_n = sqrt(n);
    init_sieve(sqrt_n);
    
    for (int64 left = 1, right, val; left <= n; left = right + 1) {
        val = n / left;
        right = n / val;
        values[++total_vals] = val;
        
        int64 v_mod = val % MOD;
        dp1[total_vals] = submod(compute_prefix_sum(v_mod), 1);
        dp2[total_vals] = submod(compute_square_sum(v_mod), 1);
        
        if (val <= sqrt_n) idx_small[val] = total_vals;
        else idx_large[n / val] = total_vals;
    }
    
    for (int i = 1; i <= prime_cnt; ++i) {
        int p = primes[i];
        int64 p_mod = p % MOD;
        int64 p2_mod = mulmod(p_mod, p_mod);
        
        for (int j = 1; j <= total_vals && 1LL * p * p <= values[j]; ++j) {
            int64 div_result = values[j] / p;
            int target_idx = (div_result <= sqrt_n) ? idx_small[div_result] 
                                                    : idx_large[n / div_result];
            
            dp1[j] = submod(dp1[j], mulmod(p_mod, submod(dp1[target_idx], sum_p1[i - 1])));
            dp2[j] = submod(dp2[j], mulmod(p2_mod, submod(dp2[target_idx], sum_p2[i - 1])));
        }
    }
    
    cout << addmod(solve_recursive(n, 0), 1) << '\n';
    return 0;
}

示例二:SPOJ DIVCNTK 本题要求计算 f(n) = d(n^k),其中 d(x) 表示约数个数。可以证明 f 是积性函数,且满足 f(p) = k + 1f(p^c) = c·k + 1。使用 Min_25 筛,取 F = 1,最后乘以 k + 1 即可。

示例三:Luogu P5493 质数前缀统计 仅需使用 Min_25 筛的前半部分进行动态规划。边界条件 g(n, 0) 是自然数幂和,可通过 O(k) 的拉格朗日插值求得。总时间复杂度 O(n^{3/4} / log n + √n · k)

示例四:LOJ 6235 区间素数个数 令 F(p) = 1,直接使用 Min_25 筛的前半部分进行 DP 即可。时间复杂度 O(n^{3/4} / log n + √n)

示例五:SPOJ DIVFACT4 要计算 d(n!),即 n! 的约数个数。经典结论表明,n! 中质数 p 的指数为 ∑_{c≥1} ⌊n/p^c⌋。因此:

d(n!) = ∏i=1π(n) (1 + ∑c≥1 ⌊n/p_i^c⌋)

对于大于 √n 的质数,它们仅贡献 1 + ⌊n/p⌋,可以通过整除分块处理:对每个块 [l, r],先用 Min_25 筛求出该区间内的质数个数 cnt,然后给答案乘上 (1 + ⌊n/l⌋)^{cnt}。时间复杂度 O(n^{3/4} / log n + √n · log n)

对于不超过 √n 的质数,直接暴力计算即可,共有 O(√n / log n) 个。

由于模数可能很大,需要使用快速乘法避免溢出。

#include <bits/stdc++.h>
using namespace std;
using int64 = long long;
using i128 = __int128_t;

const int MAXN = 5000005;
int64 mod;

int prime_cnt;
int primes[MAXN];
int prime_idx[MAXN];
bool is_composite[MAXN];
int sqrt_n;
int64 n;
int t;

int total_vals;
int64 seq[MAXN];
int idx_small[MAXN];
int idx_large[MAXN];
int64 g[MAXN];

int64 mulmod(int64 a, int64 b) {
    return (i128)a * b % mod;
}

int64 powmod(int64 a, int64 e) {
    int64 r = 1 % mod;
    a %= mod;
    while (e) {
        if (e & 1) r = mulmod(r, a);
        a = mulmod(a, a);
        e >>= 1;
    }
    return r;
}

void build_sieve(int limit) {
    is_composite[1] = true;
    for (int i = 2; i <= limit; ++i) {
        if (!is_composite[i]) {
            prime_idx[i] = ++prime_cnt;
            primes[prime_cnt] = i;
        }
        for (int j = 1; j <= prime_cnt && i * primes[j] <= limit; ++j) {
            is_composite[i * primes[j]] = true;
            prime_idx[i * primes[j]] = j;
            if (i % primes[j] == 0) break;
        }
    }
}

inline int get_index(int64 x) {
    return (x <= sqrt_n) ? idx_small[x] : idx_large[n / x];
}

int64 count_factor(int64 base, int64 p) {
    int64 res = 0;
    while (base >= p) {
        base /= p;
        res += base % mod;
    }
    return res % mod;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    build_sieve(MAXN - 5);
    cin >> t;
    
    while (t--) {
        cin >> n >> mod;
        sqrt_n = sqrt(n);
        total_vals = 0;
        int64 answer = 1 % mod;
        
        for (int64 left = 1, right, val; left <= n; left = right + 1) {
            val = n / left;
            right = n / val;
            seq[++total_vals] = val;
            g[total_vals] = (val - 1) % mod;
            
            if (val <= sqrt_n) idx_small[val] = total_vals;
            else idx_large[right] = total_vals;
        }
        
        for (int i = 1; i <= prime_cnt && primes[i] <= sqrt_n; ++i) {
            for (int j = 1; j <= total_vals && 1LL * primes[i] * primes[i] <= seq[j]; ++j) {
                int pos = get_index(seq[j] / primes[i]);
                g[j] -= (g[pos] - (i - 1));
                if (g[j] < 0) g[j] += mod;
            }
        }
        
        for (int i = 1; i <= prime_cnt && primes[i] <= sqrt_n; ++i) {
            answer = mulmod(answer, addmod(count_factor(n, primes[i]), 1));
        }
        
        for (int64 left = sqrt_n + 1, right, val; left <= n; left = right + 1) {
            right = n / (val = n / left);
            int64 cnt = g[get_index(right)] - g[get_index(left - 1)];
            if (cnt < 0) cnt += mod;
            answer = mulmod(answer, powmod((val + 1) % mod, cnt));
        }
        
        cout << answer << '\n';
    }
    return 0;
}

示例六:SPOJ APS2 这是 Min_25 筛的一种非平凡应用。需要分别处理质数和合数。

对于质数部分,要求 ∑i=1n [i ∈ ℙ] i,直接令 F(p) = p 并使用 Min_25 筛的前半部分即可。

对于合数部分,考察 Min_25 筛前半部分的 DP 公式:

g(n, x) = g(n, x-1) - F(p_x) · (g(⌊n/p_x⌋, x-1) - spx-1)

注意到 g(⌊n/p_x⌋, x-1) - sp<sub>x-1</sub> 正好统计了 n 以内最小质因子等于 p_x 的数的个数。在 DP 过程中累加这一结果,即可得到合数部分的答案。

总时间复杂度 O(n^{3/4} / log n + √n)

算法小结 Min_25 筛法是一种在数论函数求和中极为强大的工具,尤其适用于处理与质数相关的复杂求和问题。其核心在于通过构造完全积性函数进行拟合,结合容斥原理与动态规划,在 O(n^{3/4} / log n) 的时间复杂度内完成计算。该算法在竞赛编程和数论研究中都有广泛的应用价值。

标签: 数论算法

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。