最少操作数等于 n 以内无平方因子数个数;容斥化为 μ(d)·⌊n/d²⌋ 求和,对 d 分块并用杜教筛求莫比乌斯前缀和,压到 O(n^0.4)。
OJ: luogu
题目 ID: P9238
难度:省选/NOI-
标签:数论莫比乌斯函数杜教筛整除分块容斥线性筛
创建: 2026-10-02 15:07
更新: 2026-10-03 11:47
形式化题目
有
由于同一种操作做两次等于没做,我们只需决定每个
解法路线
题面给的是一组真实的分层约束:
| 层次 | 适用范围 | 核心做法 | 瓶颈 |
|---|---|---|---|
| 暴力解法 | 按定义标记含平方因子的数 | 必须遍历 |
|
| 子任务解法 | 容斥公式 + 枚举 |
枚举量 |
|
| 正解 | 对 |
块数 |
正式主解是第三层,代码为 main.cpp。
暴力解法
适用范围
思路
先解决「答案是什么」这个问题,三种规模都共用它。
令
要求位置
关键在于这个方程组的解是唯一的。按
既然解唯一,「最少操作次数」就不是一个需要搜索的最优化问题,而是这个唯一解的重量。把解写出来:莫比乌斯函数满足
这正好就是上面两组方程(
以
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
|---|---|---|---|---|---|---|---|
| 是否操作 | 是 | 是 | 是 | 否 | 是 | 是 | 是 |
剩下的事就是统计
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-02 15:07
* update_at: 2026-10-02 15:07
*/
/*
* brute.cpp:小数据暴力解,用来辅助对拍。
*
* 思路:完全不用莫比乌斯函数,直接按定义统计无平方因子数。
* 对每个素数 p <= sqrt(n),把 p^2 的所有倍数都标记成「含平方因子」,
* 最后没被标记过的数就是无平方因子数,答案就是它们的个数。
* 复杂度 O(n),适合 n <= 2e7 的对拍规模。
*
* 它和 main.cpp 的莫比乌斯 + 杜教筛路线没有任何共享逻辑,
* 是一个可以独立验证结论的暴力。
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 20000005;
static char bad[MAXN]; // bad[x] = 1 表示 x 含平方因子
static int primes[1500000];
int pcnt;
int main() {
int n;
scanf("%d", &n);
// 线性筛出 sqrt(n) 以内的素数
int lim = (int)sqrt((double)n) + 1;
vector<char> isc(lim + 1, 0);
for (int i = 2; i <= lim; i++) {
if (!isc[i]) primes[++pcnt] = i;
for (int j = 1; j <= pcnt; j++) {
long long v = (long long)primes[j] * i;
if (v > lim) break;
isc[(int)v] = 1;
if (i % primes[j] == 0) break;
}
}
// 把每个 p^2 的倍数标记为「含平方因子」
for (int j = 1; j <= pcnt; j++) {
long long sq = (long long)primes[j] * primes[j];
for (long long x = sq; x <= n; x += sq) {
bad[(int)x] = 1;
}
}
long long ans = 0;
for (int i = 1; i <= n; i++) {
if (!bad[i]) ans++;
}
printf("%lld\n", ans);
return 0;
}复杂度与瓶颈
- 时间复杂度:
,标记 倍数的总次数为 ,再加上 的素数筛和 的一次计数。 - 空间复杂度:
,用一个 char数组记录每个数是否含平方因子。
子任务解法:
适用范围
思路
把「无平方因子」写成容斥。对每个
这个式子的正确性由
long long 计算,避免 int。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-02 15:07
* update_at: 2026-10-02 15:07
*/
/*
* 子任务解法:n <= 1e9(对应题面 70% 的评测用例)。
*
* 直接使用容斥公式 Q(n) = sum_{d=1}^{floor(sqrt(n))} mu(d) * floor(n / d^2)。
* n <= 1e9 时 sqrt(n) <= 31623,线性筛出 mu 后直接枚举 d 即可,
* 复杂度 O(sqrt(n)),和 main.cpp 的杜教筛路线相比只是少了「d 很大」这一段。
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXM = 40000; // n <= 1e9 时 sqrt(n) <= 31623
static int mu[MAXM];
static int primes[MAXM];
static char isc[MAXM];
int pcnt;
int main() {
ll n;
scanf("%lld", &n);
int N = (int)sqrtl((long double)n) + 1;
if (N > MAXM - 1) N = MAXM - 1;
// 线性筛莫比乌斯函数
mu[1] = 1;
for (int i = 2; i <= N; i++) {
if (!isc[i]) {
primes[++pcnt] = i;
mu[i] = -1;
}
for (int j = 1; j <= pcnt; j++) {
ll v = (ll)primes[j] * i;
if (v > N) break;
isc[v] = 1;
if (i % primes[j] == 0) {
mu[v] = 0;
break;
}
mu[v] = -mu[i];
}
}
// 直接枚举所有 d <= sqrt(n)
ll ans = 0;
for (ll d = 1; d <= N; d++) {
ans += (ll)mu[d] * (n / (d * d));
}
printf("%lld\n", ans);
return 0;
}复杂度与瓶颈
- 时间复杂度:
,线性筛 加一次枚举 。 - 空间复杂度:
。
正解
关键观察
其中
对第二件事用杜教筛。由
把
右边按 unordered_map 记忆化,单次求
对第一件事做权衡:设前
总复杂度分三块。线性筛
取
以
| 分段 | 项数 | 贡献 | |||
|---|---|---|---|---|---|
| 直接枚举 | — | — | |||
| 分块 | |||||
| 分块 |
三段相加得到
思路
- 取
,并用数组容量兜住上界(本实现上限 ,对应 时的 )。 - 线性筛出
,在累加第一段之前先直接用 累加 的贡献,然后把数组原地改造成前缀和 ,供杜教筛查表使用。 - 算出块数
,对 分块累加;每段的下界 若小于 就抬到 。 - 杜教筛内部用
__int128累加:中 的绝对值可达 量级,两者相乘会超出 int,虽然本身一定落在 long long内,中间乘积仍需更宽的中间类型才稳妥。
求 sqrtl 估一个初值,再用整数乘法微调,避免 long double 舍入导致结果差一。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-02 15:07
* update_at: 2026-10-02 15:07
*/
/*
* P9238 [蓝桥杯 2023 省 A] 翻转硬币
*
* 结论:最少操作次数 = 1..n 中「无平方因子数」的个数。
*
* 记 x_i 表示是否使用一次操作 i(同一个操作用两次等于没用,所以 x_i 只有 0/1)。
* 硬币 j 最终被翻转的次数是 sum_{i | j} x_i,要求全部朝上:
* j = 1 : x_1 = 1
* j >= 2: sum_{i | j} x_i = 0 (mod 2)
* 按 j 从小到大看,第 j 个方程里只出现 x_i (i | j 且 i <= j),且 x_j 的系数为 1,
* 于是每个 x_j 都被唯一确定,解正是 x = mu(莫比乌斯函数)。
* 所以答案 = sum_{i = 1}^{n} [mu(i) != 0] = n 以内无平方因子数的个数。
*
* 用容斥(按最小平方因子枚举所有平方数)得到
* Q(n) = sum_{d = 1}^{floor(sqrt(n))} mu(d) * floor(n / d^2)。
* sqrt(n) 最大 1e9,不能直接枚举,于是分成两段:
* 1) d <= T:线性筛出 mu,直接累加;
* 2) d > T:按 q = floor(n / d^2) 分块。q 相同的一段可以批量算,
* 段内的莫比乌斯和用杜教筛求前缀和 M = sum mu。
* 取 T = floor(n^(2/5))(并限制在数组容量内),此时块数 Q = floor(n / (T+1)^2)
* 只有 n^(1/5) 量级,整体复杂度 O(n^(2/5)),n = 1e18 时 0.4s 左右。
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXT = 12000000; // 阈值上限,约 52MB 内存
static int mu[MAXT + 5]; // 先存 mu(i),需要时再原地改造成前缀和 M(i)
static int primes[800000]; // 素数表
static unsigned long long notprime[(MAXT >> 6) + 5]; // 合数标记位图
int pcnt; // 素数个数
ll T; // 直接计算的阈值
unordered_map<ll, ll> memo; // 杜教筛的记忆化
// 求 floor(sqrt(x)):用 long double 估一个初值,再靠整数运算微调,避免精度问题
ll isqrt_ll(ll x) {
if (x <= 0) return 0;
ll r = (ll)sqrtl((long double)x);
while ((r + 1) <= x / (r + 1)) r++;
while (r > x / r) r--;
return r;
}
// 杜教筛:莫比乌斯前缀和 M(x) = sum_{i = 1}^{x} mu(i)
// 由 mu * 1 = eps 得 1 = sum_{d = 1}^{x} M(floor(x / d)),
// 移项就有 M(x) = 1 - sum_{d = 2}^{x} M(floor(x / d)),对 d 整除分块即可
ll mertens(ll x) {
if (x <= T) return mu[x]; // 小范围查表
unordered_map<ll, ll>::iterator it = memo.find(x);
if (it != memo.end()) return it->second;
__int128 res = 1; // sum_{i = 1}^{x} eps(i) = 1
for (ll d = 2; d <= x; ) {
ll v = x / d; // 这一段的 floor(x / d) 都等于 v
ll last = x / v;
res -= (__int128)(last - d + 1) * mertens(v);
d = last + 1;
}
ll rr = (ll)res; // M(x) 的绝对值不超过 x,一定放得下
memo[x] = rr;
return rr;
}
int main() {
ll n;
scanf("%lld", &n);
// 阈值 T = min(floor(sqrt(n)), floor(n^(2/5))),并保证不越界
T = isqrt_ll(n);
ll t = (ll)powl((long double)n, 0.4L) + 5;
if (t < T) T = t;
if (T > MAXT) T = MAXT;
if (T < 1) T = 1;
int N = (int)T;
// 线性筛莫比乌斯函数,此时 mu[i] 就是 mu(i)
mu[1] = 1;
for (int i = 2; i <= N; i++) {
if (!((notprime[i >> 6] >> (i & 63)) & 1ULL)) {
primes[++pcnt] = i;
mu[i] = -1;
}
for (int j = 1; j <= pcnt; j++) {
ll v = (ll)primes[j] * i;
if (v > N) break;
notprime[v >> 6] |= 1ULL << (v & 63);
if (i % primes[j] == 0) {
mu[v] = 0; // v 含平方因子
break;
}
mu[v] = -mu[i];
}
}
// 第一段:d <= T 的部分直接累加 mu(d) * floor(n / d^2)
ll ans = 0;
for (ll d = 1; d <= T; d++) {
ans += (ll)mu[d] * (n / (d * d));
}
// 把 mu 原地改造成前缀和 M(i),供杜教筛查表
for (int i = 1; i <= N; i++) mu[i] += mu[i - 1];
// 第二段:d > T。此时 floor(n / d^2) = q <= Q,按 q 分块批量算
ll Q = n / ((T + 1) * (T + 1));
for (ll q = 1; q <= Q; q++) {
ll hi = isqrt_ll(n / q); // floor(n / d^2) >= q 的最大 d
ll lo = isqrt_ll(n / (q + 1)); // floor(n / d^2) >= q+1 的最大 d
if (lo < T) lo = T; // 只统计 d > T,避免和第一段重复
// d 在 (lo, hi] 内时 floor(n / d^2) 恰好等于 q,
// 这些 d 的 mu 之和是 M(hi) - M(lo),乘上系数 q 累加
ans += q * (mertens(hi) - mertens(lo));
}
printf("%lld\n", ans);
return 0;
}复杂度
- 时间复杂度:
。 时 、 ,实测约 s(其中线性筛 s、直接枚举 s、分块累计 s)。分块部分内部循环约 次,与 同量级,而直接枚举所有 需要 次。 - 空间复杂度:
,主要是 数组( int)、合数位图与素数表,实测峰值约MB。
正确性上,
总结
本题的难点不在实现,而在两步「翻译」:
- 把「最少操作次数」翻译成唯一解。同一操作做两次相互抵消,每个
只取 ;按 递增看,第 个同余方程里 系数为 ,于是解唯一,最小值不需要搜索。这个唯一解恰好是莫比乌斯函数,答案就是 以内的无平方因子数个数。想清楚这一点,题目就从一道操作题变成了一道数论计数题。 - 把「数出
以内的无平方因子数」翻译成可以分块的求和。容斥给出一重和 ;瓶颈是 太大,而 的取值很少,于是对 分块(块数 ),段内用杜教筛求莫比乌斯前缀和( ,整除分块 + 记忆化)。取阈值 平衡线性筛的 与分块部分的 ,总复杂度 。
三层的递进关系也值得记住:暴力解法按定义统计无平方因子数,是把「答案 = 无平方因子数个数」这个结论落到代码上最直接的写法(