蓝桥杯2023 C++大学A组 J.翻转硬币 题解
Sunlight_zero · · 题解
前置知识点
本题是数论题,需要对初等数论以下几个知识有所了解:
- Möbius 函数
\mu(n) - Dirichlet 卷积
f * g - 数论分块
- Euler 筛(线性筛)求数论函数的值
- 杜教筛求数论函数前缀和
以上知识点的学习可参考 OI Wiki。
题解
分析
题意分析
题意即每次选择一个数
同时可以看出,每个硬币只可能被它的因数(包括它本身)翻转,例如硬币
初始时硬币
然后考虑翻转编号为质数
然后考虑
继续考虑
以此类推,所有可以分解为若干个互异质因数之积的整数
下面证明除此以外的其他数(即有平方因子数)都已经翻转到正面。设有平方因子数
因此只需要统计
\mu^2 的前缀和性质
首先证明 Möbius 函数平方的一个重要性质:
证明:设
设
最后一步运用了经典 Dirichlet 卷积结论
而
然后据此推导 Möbius 函数平方的一个重要前缀和性质:
证明:将
到这一步,如果使用 Euler 筛预处理出
数论分块
我们需要进一步处理,继续降低
数论分块
数论分块的关键是,对于给定的
因此,仿照普通的数论分块,这里的数论分块代码如下:
ll prefix_mu2(size_t n)
{
// lower_sqrt 表示开方后向下取整
size_t l = 1, r, bound = lower_sqrt(n);
ll ans = 0;
while (l <= bound)
{
r = lower_sqrt(n / (n / (l * l)));
ans += (n / (l * l)) * (prefix_mu(r) - prefix_mu(l - 1)); // prefix_mu 表示 Möbius 函数前缀和
l = r + 1;
}
return ans;
}
其中 lower_sqrt 函数可以用二分法实现。
\mu 前缀和 & 杜教筛
最后只需要计算
可以预先用 Euler 筛求出
复杂度分析
杜教筛复杂度
根据 OI-Wiki 杜教筛 的结论,若预处理出前
这里由于
数论分块的分块数
由于这里使用了新的数论分块技巧,仿照经典数论分块的方法,这里
- 当
d \le n^{1 / 3} 时,\left \lfloor \frac{n}{d^2} \right \rfloor 至多有n^{1 / 3} 个值; - 当
d > n^{1 / 3} 时,\frac{n}{d^2} \le n^{1 / 3} ,由于向下取整,\left \lfloor \frac{n}{d^2} \right \rfloor 同样至多有n^{1 / 3} 个值。
每个分块都使用了二分法求开平方值,开平方的总时间复杂度是
总复杂度
在数论分块中,若预处理出前
这里设
由上式看出
如果预处理前
内存优化
预处理的数据越多,
Euler 筛需要一个 is_prime 数组记录被筛去的质数,一个 primes 记录当前得到的质数,还需要一个 mu 数组记录已经筛出的 Möbius 函数值,代码:
constexpr size_t MAXN = 1e7;
int mu[MAXN + 1], smu[MAXN + 1];
bitset<MAXN + 1> is_prime;
unsigned primes[MAXN];
void preprocess()
{
constexpr size_t n = MAXN;
size_t cnt = 0;
is_prime.set();
is_prime[0] = is_prime[1] = false;
mu[1] = 1;
for (ull i = 2; i <= n; i++)
{
if (is_prime[i])
{
primes[cnt++] = i;
mu[i] = -1;
}
for (size_t j = 0; j < cnt; j++)
{
if ((ll) i * primes[j] > n)
{
break;
}
is_prime[i * primes[j]] = false;
if (i % primes[j] == 0)
{
mu[i * primes[j]] = 0;
break;
}
else
{
mu[i * primes[j]] = -mu[i];
}
}
}
for (size_t i = 1; i <= n; i++)
{
smu[i] = smu[i - 1] + mu[i];
}
}
但 is_prime、primes 和 mu 数组都只是预处理过程的辅助变量,预处理结束后保留 Möbius 前缀和数组 smu 即可。因此,我们可以使用代码块(一个花括号),将 is_prime 和 primes 变为局部变量,程序跳出代码块时会自动销毁并释放内存。同时,在一开始就将 Möbius 函数值存储在 smu 内,后面直接原地计算前缀和即可。优化后的代码如下:
constexpr size_t MAXN = 5e7;
int smu[MAXN];
void preprocess()
{
constexpr ull n = MAXN;
{
bitset<MAXN + 1> is_prime;
vector<unsigned int> primes;
is_prime.set();
is_prime[0] = is_prime[1] = false;
smu[1] = 1;
for (ull i = 2; i <= n; i++)
{
if (is_prime[i])
{
primes.push_back(i);
smu[i] = -1;
}
for (unsigned int p: primes)
{
if ((ull) i * p > n)
{
break;
}
is_prime[i * p] = false;
if (i % p == 0)
{
smu[i * p] = 0;
break;
}
else
{
smu[i * p] = -smu[i];
}
}
}
}
for (size_t i = 1; i <= n; i++)
{
smu[i] += smu[i - 1];
}
}
这里开 int 数组,占用内存为
代码
汇总以上全部思想,完整的算法步骤如下:
- 用 Euler 筛预处理
k \le 10^7 的 Möbius 函数值及其前缀和; - 编写函数,使用杜教筛处理
k > 10^7 的 Möbius 函数前缀和; - 使用数论分块计算
\mu^2 的前缀和。
#include <iostream>
#include <unordered_map>
#include <bitset>
#include <vector>
using namespace std;
using ll = long long;
using ull = unsigned long long;
constexpr size_t MAXN = 5e7;
constexpr size_t INF = 1e9;
int smu[MAXN];
void preprocess()
{
// Euler seive
constexpr ull n = MAXN;
{
bitset<MAXN + 1> is_prime;
vector<unsigned int> primes;
is_prime.set();
is_prime[0] = is_prime[1] = false;
smu[1] = 1;
for (ull i = 2; i <= n; i++)
{
if (is_prime[i])
{
primes.push_back(i);
smu[i] = -1;
}
for (unsigned int p: primes)
{
if ((ull) i * p > n)
{
break;
}
is_prime[i * p] = false;
if (i % p == 0)
{
smu[i * p] = 0;
break;
}
else
{
smu[i * p] = -smu[i];
}
}
}
}
// Calculate the prefix sum
for (size_t i = 1; i <= n; i++)
{
smu[i] += smu[i - 1];
}
}
unordered_map<size_t, ll> fmu; // Store calculated prefix sums of mu
ll prefix_mu(size_t n)
{
if (n <= MAXN)
{
return smu[n];
}
else if (fmu.find(n) != fmu.end()) // This value has been calculated
{
return fmu[n];
}
// Dujiao seive
size_t l = 2, r;
ll ans = 1;
while (l <= n)
{
r = n / (n / l);
ans -= (r - l + 1) * prefix_mu(n / l);
l = r + 1;
}
fmu[n] = ans;
return ans;
}
// return the largest number x that x ^ 2 <= y
ull lower_sqrt(ull y)
{
size_t l = 1, r = INF;
ll ans;
while (l <= r)
{
size_t mid = (l + r) / 2;
if (mid * mid <= y)
{
ans = mid;
l = mid + 1;
}
else
{
r = mid - 1;
}
}
return ans;
}
// Calculate the result
ull prefix_mu2(size_t n)
{
size_t l = 1, r, bound = lower_sqrt(n);
ll ans = 0;
ll last_predix = 0, this_predix;
while (l <= bound)
{
r = lower_sqrt(n / (n / (l * l)));
this_predix = prefix_mu(r);
ans += (n / (l * l)) * (this_predix - last_predix);
l = r + 1;
last_predix = this_predix;
}
return ans;
}
int main()
{
ios::sync_with_stdio(false);
size_t n;
cin >> n;
preprocess();
cout << prefix_mu2(n) << '\n';
return 0;
}
扩展
在这个程序中,输入比较大的
解析数论的研究内容之一就是数论函数的前
在维基百科的 Square-free integer 条目中提到,
通过这个结论也可以验证算法在
写在最后
本题解也同步发表于知乎。
本题解的主要思想来自知乎文章 https://zhuanlan.zhihu.com/p/521413862 和超理论坛讨论 https://chaoli.club/index.php/8624 ,经过自己的思考、测试和细节补充最终得到这份题解。
本 ACM 蒟蒻只是初步涉猎初等数论,目前也只想到这一种解法,欢迎各位大佬在评论区一起交流