莫比乌斯函数
莫比乌斯函数
主要是 莫比乌斯反演,前面已经 用过几次了,但是没有 专门讲
建议阅读 数论函数基础,数论分块,欧拉函数 章节,了解基本概念与先要知识
全文 绝大多数 内容是对 [0] 中讲述的 粗略抄写 和 胡乱加工
1. 概念
关于 莫比乌斯函数,前面 数论函数基础 章节中有所提到,这里只给出 定义式
判定是否等于
,是否有 平方因子
但其实它最初被发现的时候长这样(在 数论函数基础 中作为一个 结论 提出了)
一种转化过程在 数论函数基础 一节中已给出,考虑展开后 二项式定理 即可,这里不做赘述
下面会描述另外一种转化方式
上述式子经常以
在
这就是我们常说的 莫比乌斯反演,考虑证明
先来考虑
其中
比较有趣,注意到 ,故 与 一定 对称且一一对应
然后考虑
对于
,容易发现,下面的 等效于上面的 ,于是
于是得证
我们可以用 狄利克雷卷积 很好的表示 莫比乌斯反演 的式子
于是回到 发现时的式子,我们发现其可以表示为
注意到 积性函数的逆元也是积性函数,故由此我们也可以说明 莫比乌斯函数是积性函数
根据 积性函数的性质,我们知道我们只需得出
当
容易得到
我们继续对 欧拉函数 一节中的 欧拉反演 加以探究(证明时笔者就采取了 莫比乌斯反演 的思路)
注意到欧拉反演可以写成
其中
是 恒等函数 / 幂函数,在 数论函数基础 一节中有所介绍
于是使用 莫比乌斯反演 即可得到
我们也可以从 容斥系数 的角度来理解 莫比乌斯函数,具体请参考 [0] 中内容,此处不表
2. 线性筛莫比乌斯函数
我们知道了 莫比乌斯函数 是 积性函数,再根据 定义式,线性筛是简单的
inline void Sieve () {
mu[1] = 1;
for (int i = 2; i <= N; ++ i) {
if (!vis[i]) pri[++ Cnt] = i, mu[i] = - 1;
for (int k = 1; k <= Cnt && pri[k] * i <= N; ++ k) {
vis[pri[k] * i] = 1;
if (i % pri[k] == 0) break ;
mu[i * pri[k]] = - mu[i];
}
}
}
3. Tricks
在应用 莫比乌斯反演 的时候,我们常常可以见到类似下面式子的形式
这其实就是 上面提到过的 发现时的式子的等价形式
而有一个经典的套路,在需要枚举
显然,当且仅当
注意,是
整除 与 整除 ,不是 或 与 或
Trick 1
我们可以这样解决下面的部分(这是很常见的)
这个式子就可以简单的用 整除分块 在
对于上面的变换,我们有一种 感性的理解方式
相当于我们对
的 因数 进行了 容斥 即加上了
的数的贡献,减去 的贡献 加上
的贡献... 容易发现,这样的 容斥系数 就是
Trick 2
还有一个常用的 等式 如下
一个证明思路是 分离质因子,考虑
于是显然有
若有
若有
显然当
故可以证得这个结论
Trick 2 - ex
根据上面的证明,我们可以容易将这个结论拓展到
同样对质因子 分开考虑,然后对
我们设
那么对于任意因数
若
容易证明满足 互质条件,同时 质因数间独立,这样的构造形成一个 双射,那么结论成立
为什么这样的构造是一个 双射(因数 与 取法 一一对应)
感性理解,一个质因数
,显然只能被一个 包含(均不考虑 的情况) 在
的因数中,这个 有 种可能状态( ) 恰好要使得一个
包含 ,我们仅有 种可能方式 即
包含 次,其余 不包含( ),故我们可以 一一对应
Trick 3
对左式 莫比乌斯反演
转换枚举顺序
去掉一个
再移动一下
后半部分就是一个
Trick 3 - ex
其实就是下面 Luogu P3700 [CQOI2017] 小 Q 的表格 这个题的 重要结论
但是这个式子确实 十分简洁,也比较常见,就在这里记录一下
显然
然后使用 Trick 3,带入到
简单化简一下,注意到
4. 例题
巨大多多多多题,可能有各种技巧 / 算法的复合
由于例题过多,部分存在于 [0] 但 与其他问题相似 / 被包含 的问题就被 跳过了
可能后面有时间会补
Luogu P2522 [HAOI2011] Problem b
直接形式化题意是这样的
多次询问上述式子的值,每次给定
乍一看感觉这东西 啥也不是,于是我们进行简单转化,显然,这个式子可以差分
于是我们只需考虑形如
集中注意力,我们发现它和 Trick 1 十分相似,只有
于是应用 Trick 1,就可以转化成下面的最终形式
然后可以 整除分块 做掉,时间复杂度
#include <iostream>
#include <cstdint>
const int MAXN = 50005;
int mu[MAXN], pri[MAXN], Cnt = 0;
bool vis[MAXN];
inline void Sieve (const int n = 50000) {
mu[1] = 1;
for (int i = 2; i <= n; ++ i) {
if (!vis[i]) pri[++ Cnt] = i, mu[i] = - 1;
for (int k = 1; k <= Cnt && pri[k] * i <= n; ++ k) {
vis[pri[k] * i] = 1;
if (i % pri[k] == 0) break ;
mu[pri[k] * i] = - mu[i];
}
}
for (int i = 1; i <= n; ++ i) mu[i] += mu[i - 1];
}
int a, b, c, d, k;
inline int64_t Calc (int n, int m) {
int64_t d, v, ans = 0; n /= k, m /= k;
for (int l = 1, r; l <= std::min (n, m); l = r + 1) {
r = std::min (m / (m / l), n / (n / l)), d = m / l, v = n / l;
ans += d * v * (mu[r] - mu[l - 1]);
}
return ans;
}
int N;
int main () {
std::cin >> N, Sieve ();
for (int i = 1; i <= N; ++ i) {
std::cin >> a >> b >> c >> d >> k;
std::cout << Calc (b, d) + Calc (a - 1, c - 1) - Calc (a - 1, d) - Calc (b, c - 1) << '\n';
}
return 0;
}
EXP - 1 - Luogu P3455 [POI2007] ZAP-Queries(
Luogu P2257 YY的GCD
形式化题意是这样的
多次询问,每次给定
这个
然后 套路上界除以
使用 Trick 1,转化
现在时间复杂度是
显然,这时候瓶颈就在于 枚举质数 的部分了,考虑怎么 消除其影响,我们设
于是上式可以转化成
更进一步的,交换枚举顺序后,我们有
于是前半部分直接 整除分块 就可以在
后半部分看上去是 困难的,但事实上我们在 线性筛 中可以 预处理 其值,计算 前缀和
(看成一个函数
注意 线性筛 中
i % pri[k] == 0
的特殊处理考虑此时
i * pri[k]
中一定有pri[k] ^ 2
这个 平方因数故
,只需考虑 的情况 即
的值
然后就可以 获取胜利 了,时间复杂度
#include <iostream>
#include <cstdint>
const int MAXN = 10000005;
int pri[MAXN], mu[MAXN], ff[MAXN], Cnt = 0;
bool vis[MAXN];
inline void Sieve (const int n = MAXN - 5) {
mu[1] = 1;
for (int i = 2; i <= n; ++ i) {
if (!vis[i]) pri[++ Cnt] = i, mu[i] = - 1, ff[i] = 1;
for (int k = 1; k <= Cnt && pri[k] * i <= n; ++ k) {
vis[pri[k] * i] = 1;
if (i % pri[k] == 0) {
ff[pri[k] * i] = mu[i];
break ;
}
mu[pri[k] * i] = - mu[i];
ff[pri[k] * i] = - ff[i] + mu[i];
}
}
for (int i = 1; i <= n; ++ i) ff[i] += ff[i - 1];
}
inline int64_t Calc (const int n, const int m) {
int64_t ans = 0, d, v;
for (int l = 1, r; l <= std::min (n, m); l = r + 1) {
r = std::min (n / (n / l), m / (m / l)), d = n / l, v = m / l;
ans += d * v * (ff[r] - ff[l - 1]);
}
return ans;
}
int T;
int a, b;
int main () {
std::cin >> T, Sieve ();
for (int i = 1; i <= T; ++ i) {
std::cin >> a >> b;
std::cout << Calc (a, b) << '\n';
}
return 0;
}
Luogu P4318 完全平方数
考虑设
可以考虑 求出来之后二分答案,考虑怎么快速求解
于是考虑每个 质数
而在此之后,显然整除
以此类推,容易发现这样 容斥系数 就是
直接做就可以了,不需要整除分块,套上二分,时间复杂度
我们的
预处理到 就行了(保证 能取到 的 ),时间正确
#include <bits/stdc++.h>
const int MAXN = 65540;
int pri[MAXN], mu[MAXN], cnt;
bool vis[MAXN];
inline void Sieve (const int n = 1 << 16) {
mu[1] = 1;
for (int i = 2; i <= n; ++ i) {
if (!vis[i]) pri[++ cnt] = i, mu[i] = - 1;
for (int k = 1; k <= cnt && pri[k] * i <= n; ++ k) {
vis[i * pri[k]] = 1;
if (i % pri[k] == 0) break ;
mu[i * pri[k]] = - mu[i];
}
}
}
inline int F (const int n) {
int ans = 0;
for (int i = 1; i <= sqrt (n); ++ i) ans += mu[i] * (n / i / i);
return ans;
}
inline int G (const int n) {
int l = 0, r = INT_MAX, m = 0, ans = 0;
while (r > l) {
m = (0ll + l + r) >> 1;
if (F (m) >= n) r = ans = m;
else l = m + 1;
}
return ans;
}
int N, k;
int main () {
std::cin >> N, Sieve ();
for (int i = 1; i <= N; ++ i) {
std::cin >> k;
std::cout << G (k) << '\n';
}
return 0;
}
Luogu P3327 [SDOI2015] 约数个数和
题面好像就挺 形式化 的
多次询问上式的取值,每次给出
显然
改变枚举顺序,容易得到等价形式
注意到后面的
显然枚举
通过枚举
容易发现,后面部分两个和式可以变成 同构的部分
显然,对于一段使得
于是我们可以枚举
故对于固定
之后每组询问同样 整除分块(对
总时间复杂度
#include <iostream>
#include <cstdint>
const int MAXN = 50005;
int64_t f[MAXN];
int pri[MAXN], mu[MAXN], cnt = 0;
bool vis[MAXN];
inline void Sieve (const int n = 50000) {
mu[1] = 1;
for (int i = 2; i <= n; ++ i) {
if (!vis[i]) pri[++ cnt] = i, mu[i] = - 1;
for (int k = 1; k <= cnt && pri[k] * i <= n; ++ k) {
vis[pri[k] * i] = 1;
if (i % pri[k] == 0) break ;
mu[i * pri[k]] = - mu[i];
}
}
for (int i = 1; i <= n; ++ i) mu[i] += mu[i - 1];
for (int i = 1; i <= n; ++ i) {
int64_t ans = 0;
for (int l = 1, r; l <= i; l = r + 1)
r = i / (i / l), ans += (r - l + 1) * (i / l);
f[i] = ans;
}
}
inline int64_t Calc (const int n, const int m) {
int64_t ans = 0;
for (int l = 1, r; l <= std::min (n, m); l = r + 1)
r = std::min (n / (n / l), m / (m / l)), ans += f[n / l] * f[m / l] * (mu[r] - mu[l - 1]);
return ans;
}
int T, a, b;
int main () {
std::cin >> T, Sieve ();
for (int i = 1; i <= T; ++ i) {
std::cin >> a >> b;
std::cout << Calc (a, b) << '\n';
}
return 0;
}
Luogu P4619 [SDOI2018] 旧试题
看上去很像 上个题 的加强版
运用 Trick 2 - ex,我们可以将之转化为下式
注意到
与 不等价
与上面的题相同的套路,我们容易的将式子转化成下面的样子
然后变换枚举顺序, 注意 整除条件的取,有
同样的套路,注意到
显然,后面的部分又可以看作
暴力枚举
具体而言,可能有贡献 当且仅当
为什么
只需要限制小于 就行,不需要分成三组限制 分成三组 不便于统计,而所有不满足实际限制的,其
部分一定为 ,故 贡献是对的
于是对于每个满足上述条件的 二元组,我们将其 连边
尝试一下,发现这样的边数至多
(一说 )条,好像很可做 暴力连边是
的,我们也不是很喜欢,考虑优化 仍然枚举
,然后在一个 下枚举两数(同时 限制上界) 这样的话连边就是
的了
显然构建的图中的每个 三元环 就代表了一组 符合条件的三元组
于是使用 神秘三元环计数 的套路,以 度数 为第一关键字,编号 为第二关键字 从大到小 枚举
可以把枚举复杂度降到
所以最后我们还需要对找出的每组 三元环三个点 交换顺序计算贡献,然后就做完了
时间复杂度是
#include <iostream>
#include <cstdint>
#include <vector>
#include <unordered_map>
#include <algorithm>
const int MAXN = 100005;
const int MOD = 1000000007;
int64_t f[MAXN];
int pri[MAXN], mu[MAXN], cnt = 0;
bool vis[MAXN];
inline void Sieve (const int n = 100000) {
mu[1] = 1;
for (int i = 2; i <= n; ++ i) {
if (!vis[i]) pri[++ cnt] = i, mu[i] = - 1;
for (int k = 1; k <= cnt && pri[k] * i <= n; ++ k) {
vis[pri[k] * i] = 1;
if (i % pri[k] == 0) break ;
mu[i * pri[k]] = - mu[i];
}
}
for (int i = 1; i <= n; ++ i) {
int64_t ans = 0;
for (int l = 1, r; l <= i; l = r + 1)
r = i / (i / l), ans += (r - l + 1) * (i / l);
f[i] = ans;
}
}
struct Node {
int u, v, w;
} E[MAXN << 4];
int32_t Deg[MAXN], Vis[MAXN];
int32_t Max = 0, tot = 0;
std::vector <Node> G[MAXN];
std::unordered_map <int, bool> Mp[MAXN];
inline int64_t RetAns (const int a, const int b, const int c, const int x, const int y, const int z) {
return (1ll * f[a / x] * f[b / y] % MOD * f[c / z] % MOD) % MOD;
}
int T, A, B, C;
inline int64_t GetAns (const int d1, const int d2, const int d3, const int lcm1, const int lcm2, const int lcm3) {
int64_t ans = 0;
if (d1 == d2 && d2 == d3) ans += RetAns (A, B, C, lcm1, lcm2, lcm3);
else if (d1 == d2 || d2 == d3 || d1 == d3) ans += RetAns (A, B, C, lcm1, lcm2, lcm3), ans += RetAns (C, A, B, lcm1, lcm2, lcm3), ans += RetAns (B, C, A, lcm1, lcm2, lcm3);
else ans += RetAns (A, B, C, lcm1, lcm2, lcm3), ans += RetAns (A, C, B, lcm1, lcm2, lcm3), ans += RetAns (B, C, A, lcm1, lcm2, lcm3), ans += RetAns (B, A, C, lcm1, lcm2, lcm3), ans += RetAns (C, A, B, lcm1, lcm2, lcm3), ans += RetAns (C, B, A, lcm1, lcm2, lcm3);
ans = ans % MOD, ans = (mu[d1] * mu[d2] * mu[d3] * ans % MOD + MOD) % MOD;
return ans;
}
inline int64_t calc () {
int64_t ans = 0;
for (int i = 1; i <= tot; ++ i) {
__builtin_prefetch (& E[i]);
if (Deg[E[i].u] == Deg[E[i].v] ? E[i].u > E[i].v : Deg[E[i].u] > Deg[E[i].v])
std::swap (E[i].u, E[i].v);
G[E[i].u].push_back (E[i]);
}
for (int i = 1; i <= Max; ++ i) {
for (auto x : G[i]) Vis[x.v] = x.w;
for (auto x : G[i])
for (auto k : G[x.v])
if (Vis[k.v])
ans += GetAns (i, x.v, k.v, Vis[k.v], x.w, k.w), ans %= MOD;
for (auto x : G[i]) Vis[x.v] = 0;
}
return ans;
}
inline int64_t Calc (const int n, const int m, const int k) {
int64_t ans = 0;
Max = std::max ({n, m, k}), tot = 0;
for (int i = Max; i >= 1; -- i) {
if (mu[i]) {
for (int j = i; j <= Max; j += i) {
if (mu[j]) {
for (int k = j; k <= 1ll * Max * i / j; k += i) {
if (mu[k]) {
if (!Mp[j][k]) ++ tot, E[tot] = {j, k, j / i * k}, ++ Deg[j], ++ Deg[k], Mp[j][k] = 1;
}
}
}
}
}
}
ans += calc ();
for (int i = 1; i <= Max; ++ i) G[i].clear (), Mp[i].clear (), Deg[i] = 0;
return (ans + MOD) % MOD;
}
int main () {
std::cin >> T, Sieve ();
for (int i = 1; i <= T; ++ i) {
std::cin >> A >> B >> C;
std::cout << Calc (A, B, C) << '\n';
}
return 0;
}
EXP - 1 - CF235E Number Challenge
EXP - 2 - CF236B Easy Number Challenge
Luogu P5518 [MtOI2019] 幽灵乐团 / 莫比乌斯反演基础练习题
👻🎶👻👻👻👻🎶🎶🎶🎶🎶👻🎶🎶🎶🎶👻🎶🎶🎶🎶🎶🎶🎶🎶🎶👻🎶🎶🎶🎶🎶🎶🎶🎶🎶👻👻👻👻👻👻👻👻🎶🐨
这个题在赛时怎么是评的 紫 啊,逆大天
看这玩意儿,三重循环,还是
做过 Luogu P5221 Product 的会发现 眼前这个题又是一个 加强版,但可惜我 没有做过
分了三个
我们
然后显然 分子这部分可以单独搞掉,就可以把分子化成
显然,
现在的难点在于处理 下面的式子(即 分母 上两个 同构 的部分)
只要能在 合理的时间复杂度内 做掉这部分,那么
这时候,做过 Luogu P3704 [SDOI2017] 数字表格 的就会发现这部分和那个题 本质相同
但是我还是没有做过,乐,于是只能 开始推神秘狮子
和做
然后到了经典 Trick 1 时间,套上就行
接着,显然对于一段
然后我们就可以
于是此时
这部分的代码
namespace Type_0_1 {
int64_t f[MAXN], rf[MAXN];
inline void Init (const int n = 100000) {
f[0] = rf[0] = 1;
for (int i = 1; i <= n; ++ i) f[i] = f[i - 1] * i % MOD;
for (int i = 1; i <= n; ++ i) rf[i] = qpow (f[i]);
}
inline int64_t calc (const int n, const int m) {
int64_t ans = 0, d = 0, v = 0;
for (int l = 1, r; l <= std::min (n, m); l = r + 1) {
r = std::min (n / (n / l), m / (m / l)), d = n / l, v = m / l;
ans += 1ll * (s[r] - s[l - 1]) * d * v, ans = (ans % (MOD - 1) + (MOD - 1)) % (MOD - 1);
}
return ans % (MOD - 1);
}
inline int64_t Calc (const int n, const int m) {
int64_t ans = 1, d = 0, v = 0;
for (int l = 1, r; l <= std::min (n, m); l = r + 1) {
r = std::min (n / (n / l), m / (m / l)), d = n / l, v = m / l;
ans = ans * qpow (f[r] * rf[l - 1] % MOD, calc (d, v)) % MOD;
}
return qpow (ans); // NOTE 1 / ans
}
inline int64_t Solve (const int64_t a, const int64_t b, const int64_t c) {
return qpow (f[a], b * c) * qpow (f[b], a * c) % MOD * qpow (Calc (a, b), c) % MOD * qpow (Calc (a, c), b) % MOD;
}
}
这个东西的时间复杂度证明十分有趣,稍不注意就可能会当成
甚至 的了 注意到我们 内层整除分块
calc1
由于每次用了 快速幂,所以时间是的 而 外层整除分块
calc2
调用了次 calc1
,调用完之后快速幂所以外层这玩意儿时间是
的,重点就在于这个 了 这个
实际上等于式子里的 ,也就是程序里的 d = n / l, v = m / l
那么这东西取值一共
个,其中值 小于 的有 个 对于这部分,
,复杂度 而对于值大于
的 个东西, 于是时间复杂度是
的 而
,故 故这一部分时间复杂度也是
的,总时间复杂度就是 的了
看到这个 Type_0_1
,有人知道不对劲了,有什么没有完成的部分呢?
这东西 还不够快,特别是当有人告诉你 后面还要用 的时候,于是我们得 再进一步
我们把
为啥拿下来之后 上界就变了?
额,其实 有意义的上界 是没变的,只是这样写方便后续运算,而且
当
时, , 必有一个为 ,故 不做贡献,值是不变的
然后经典的令
我们发现
这个就很牛了,我们枚举
然后同样的 整除分块,我们就能在
注意 指数上的取模是对 MOD - 1
而非 MOD
,然后 不要把样例的模数看错
哦对,n / l * m / l != (n / l) * (m / l)
,最后是这部分代码
namespace Type_0_2 {
int64_t f[MAXN], rf[MAXN], g[MAXN], rg[MAXN];
inline void Init (const int n = 100000) {
g[0] = 1, f[0] = rf[0] = 1;
for (int i = 1; i <= n; ++ i) g[i] = g[i - 1] * i % MOD;
for (int i = 1; i <= n; ++ i) rg[i] = qpow (g[i]);
for (int i = 1; i <= n; ++ i) f[i] = 1;
for (int i = 1; i <= n; ++ i)
for (int j = 1, k = i; k <= n; ++ j, k = i * j) {
if (mu[j] == + 1) f[k] = f[k] * i % MOD;
if (mu[j] == - 1) f[k] = f[k] * rg[i] % MOD * g[i - 1] % MOD;
}
for (int i = 2; i <= n; ++ i) f[i] = f[i] * f[i - 1] % MOD;
for (int i = 1; i <= n; ++ i) rf[i] = qpow (f[i]);
}
inline int64_t Calc (const int n, const int m) {
int64_t ans = 1, d = 0, v = 0;
for (int l = 1, r; l <= std::min (n, m); l = r + 1) {
r = std::min (n / (n / l), m / (m / l)), d = n / l, v = m / l;
ans = ans * qpow (f[r] * rf[l - 1] % MOD, d * v % (MOD - 1)) % MOD;
}
return qpow (ans); // NOTE 1 / ans
}
inline int64_t Solve (const int64_t a, const int64_t b, const int64_t c) {
return qpow (g[a], b * c) * qpow (g[b], a * c) % MOD * qpow (Calc (a, b), c) % MOD * qpow (Calc (a, c), b) % MOD;
}
}
,
我们还是先把
同样的思路,我们把
前两项直接做是
于是预处理
然后就是 后面一大堆,显然,分母两个括号内 仍然同构,也就是下面的形式
据说这东西 后面没有用,于是这里只追求一个单次
继续枚举
多了个
我们再预处理一下
这样的话就和
namespace Type_1_1 {
int64_t f[MAXN], rf[MAXN], g[MAXN], h[MAXN];
inline void Init (const int n = 100000) {
f[0] = rf[0] = 1, h[0] = 1;
for (int i = 1; i <= n; ++ i) h[i] = qpow (i, i);
for (int i = 1; i <= n; ++ i) h[i] = h[i] * h[i - 1] % MOD;
for (int i = 1; i <= n; ++ i) f[i] = qpow (i, 1ll * i * i);
for (int i = 1; i <= n; ++ i) f[i] = f[i] * f[i - 1] % MOD;
for (int i = 1; i <= n; ++ i) g[i] = mu[i] * i * i % (MOD - 1);
for (int i = 1; i <= n; ++ i) g[i] = g[i] + g[i - 1], g[i] = g[i] % (MOD - 1);
for (int i = 1; i <= n; ++ i) rf[i] = qpow (f[i]);
}
inline int64_t sum (const int64_t a) {
return (a * (a + 1) / 2) % (MOD - 1);
}
inline int64_t calc (const int n, const int m) {
int64_t ans = 0, d = 0, v = 0;
for (int l = 1, r; l <= std::min (n, m); l = r + 1) {
r = std::min (n / (n / l), m / (m / l)), d = n / l, v = m / l;
ans += (g[r] - g[l - 1] + MOD - 1) % (MOD - 1) * sum (d) % (MOD - 1) * sum (v) % (MOD - 1), ans = ans % (MOD - 1);
}
return (ans + MOD - 1) % (MOD - 1);
}
inline int64_t Calc (const int n, const int m) {
int64_t ans = 1, d = 0, v = 0;
for (int l = 1, r; l <= std::min (n, m); l = r + 1) {
r = std::min (n / (n / l), m / (m / l)), d = n / l, v = m / l;
ans = ans * qpow (f[r] * rf[l - 1] % MOD, calc (d, v)) % MOD;
}
return qpow (ans); // NOTE 1 / ans
}
inline int64_t Solve (const int64_t a, const int64_t b, const int64_t c) {
return qpow (h[a], sum (b) * sum (c)) * qpow (h[b], sum (a) * sum (c)) % MOD * qpow (Calc (a, b), sum (c)) % MOD * qpow (Calc (a, c), sum (b)) % MOD;
}
}
注意这部分要预处理 三个东西,
, 和
有
对上界变了有疑问看上面
同样令
于是
显然
于是对
namespace Type_1_2 {
int64_t f[MAXN], rf[MAXN], g[MAXN], rg[MAXN], h[MAXN];
inline void Init (const int n = 100000) {
h[0] = 1, g[0] = 1, f[0] = rf[0] = 1;
for (int i = 1; i <= n; ++ i) g[i] = g[i - 1] * i % MOD;
for (int i = 1; i <= n; ++ i) rg[i] = qpow (g[i]);
for (int i = 1; i <= n; ++ i) h[i] = qpow (i, i);
for (int i = 1; i <= n; ++ i) h[i] = h[i] * h[i - 1] % MOD;
for (int i = 1; i <= n; ++ i) f[i] = 1;
for (int i = 1; i <= n; ++ i)
for (int j = 1, k = i; k <= n; ++ j, k = i * j) {
if (mu[j] == + 1) f[k] = f[k] * i % MOD;
if (mu[j] == - 1) f[k] = f[k] * rg[i] % MOD * g[i - 1] % MOD;
}
for (int i = 1; i <= n; ++ i) f[i] = qpow (f[i], 1ll * i * i);
for (int i = 2; i <= n; ++ i) f[i] = f[i] * f[i - 1] % MOD;
for (int i = 1; i <= n; ++ i) rf[i] = qpow (f[i]);
}
inline int64_t sum (const int64_t a) {
return (a * (a + 1) / 2) % (MOD - 1);
}
inline int64_t Calc (const int n, const int m) {
int64_t ans = 1, d = 0, v = 0;
for (int l = 1, r; l <= std::min (n, m); l = r + 1) {
r = std::min (n / (n / l), m / (m / l)), d = n / l, v = m / l;
ans = ans * qpow (f[r] * rf[l - 1] % MOD, sum (d) * sum (v) % (MOD - 1)) % MOD;
}
return qpow (ans); // NOTE 1 / ans
}
inline int64_t Solve (const int64_t a, const int64_t b, const int64_t c) {
return qpow (h[a], sum (b) * sum (c)) * qpow (h[b], sum (a) * sum (c)) % MOD * qpow (Calc (a, b), sum (c)) % MOD * qpow (Calc (a, c), sum (b)) % MOD;
}
}
, ,
前面把
这下前两项由于 指数上有
我们先枚举
对于
再把
同样的,我们只要
然后看到
这样
这里需要一些注意力,考虑指数上有
可能上式指数上并没有这样的东西,但是把底数的
放上去就行了
于是就成了
但现在这个
这样就很牛了,前面后面都可以 简单整除分块 完成,需要的只有
这时候咱 先别急着想第一部分咋办,我们先来处理 分母上的东西,两个这样 同构的式子
我们继续枚举
枚举
对
枚举
然后经典
于是又可以把
这时候有人就发现了,欸这 第一部分 不是和上面的一样的吗?还真是
再仔细思考一下发现上面的那个是在 分子上的,而这个是在 分母上的,两者刚好 抵消了
于是这一部分我们 都 可以 不用考虑,只管 后面的部分,注意到其中
其实就是
然后对于
namespace Type_2_1 {
int64_t f[MAXN], g[MAXN], rg[MAXN];
inline void Init (const int n = 100000) {
g[0] = rg[0] = 1; f[0] = 0;
for (int i = 1; i <= n; ++ i) g[i] = g[i - 1] * i % MOD;
for (int i = 1; i <= n; ++ i) rg[i] = qpow (g[i]);
}
inline int64_t Calc1 (const int a, const int b, const int c) {
int64_t ans = 1, d = 0, v = 0;
for (int i = 1; i <= std::max ({a, b, c}); ++ i) f[i] = (f[i - 1] + 1ll * phi[i] * (c / i) % (MOD - 1)) % (MOD - 1);
for (int l = 1, r; l <= std::min (a, b); l = r + 1) {
r = std::min (a / (a / l), b / (b / l)), d = a / l, v = b / l;
ans = ans * qpow (qpow (Type_0_2::Calc (d, v)), (f[r] - f[l - 1] + MOD - 1) % (MOD - 1)) % MOD;
}
return qpow (ans);
}
inline int64_t Calc2 (const int a, const int b, const int c) {
int64_t ans = 1, d = 0, v = 0, k = 0;
for (int l = 1, r; l <= std::min ({a, b, c}); l = r + 1) {
r = std::min ({a / (a / l), b / (b / l), c / (c / l)}), d = a / l, v = b / l, k = c / l;
ans = ans * qpow (g[d], 1ll * (t[r] - t[l - 1] + MOD - 1) % (MOD - 1) * v % (MOD - 1) * k % (MOD - 1)) % MOD;
}
return ans;
}
inline int64_t Solve (const int a, const int b, const int c) {
return Calc2 (a, b, c) * Calc2 (b, a, c) % MOD * Calc1 (a, b, c) % MOD * Calc1 (a, c, b) % MOD;
}
}
, 前面所有代码中
s
数组是的前缀和, t
数组是的前缀和
虽然但是,我们似乎没有 更快的做法了,但是,这个
有没有什么 更加好的推狮子方法呢?看到这一大堆
枚举
莫比乌斯反演,然后进行一套
最后我们只需要
然后发现中间就是
namespace Type_2_2 {
inline int64_t Solve (const int a, const int b, const int c) {
int64_t ans = 1, d = 0, v = 0, k = 0;
for (int l = 1, r; l <= std::min ({a, b, c}); l = r + 1) {
r = std::min ({a / (a / l), b / (b / l), c / (c / l)}), d = a / l, v = b / l, k = c / l;
ans = ans * qpow (Type_0_2::Solve (d, v, k), (t[r] - t[l - 1] + MOD - 1) % (MOD - 1)) % MOD;
}
return ans;
}
}
其实按照 前面纯纯莫比乌斯反演 的那个做法,我们把指数上的
都换成 然后你就会惊奇的发现 分子部分 与 分母部分 都长成
这样的形式,于是我们观察
,分子上是 ,分母是 于是最后就可以变回
这个东西,就和 推出来的结果 一模一样 了
出题人只需要考虑怎么写式子就可以了,而选手要考虑的东西就很多了
Luogu P3700 [CQOI2017] 小 Q 的表格
题意比较复杂,建议看 原题面,这个题似乎需要神秘的 注意力
我们先根据
你 发现 左边这东西 不大对称,先给它补个
注意到 右边这个
考虑
于是最终得到的 可能更加有效 一些的式子就是
这个式子告诉我们一个什么性质?
我们修改
可以对应到 的修改上, 同样,查询
的值也可以通过查询 的值得到,这样我们就把范围 缩小了
条件式 好像没办法转化了,这时候来考虑 答案式
套一下 条件式,得到
显然 枚举一手
然后经典的枚举
显然,上式分成了
因为修改在前面部分,故我们考虑 数据结构维护,现在只需要一个 快速求后面部分 的方法
也就是要转化下面这个式子
直接使用 Trick 3 - ex 即可得到
如果我们对
考虑询问次数
可以想到
所以笔者这里直接
注意
long long
的取
#include <bits/stdc++.h>
const int MAXN = 4000005;
const int MOD = 1000000007;
int64_t f[MAXN];
int pri[MAXN], phi[MAXN], mu[MAXN], cnt = 0;
bool vis[MAXN];
inline void Sieve (const int n = 4000000) {
mu[1] = phi[1] = 1;
for (int i = 2; i <= n; ++ i) {
if (!vis[i]) pri[++ cnt] = i, phi[i] = i - 1, mu[i] = - 1;
for (int k = 1; k <= cnt && pri[k] * i <= n; ++ k) {
vis[pri[k] * i] = 1;
if (i % pri[k] == 0) {
phi[i * pri[k]] = phi[i] * pri[k];
break ;
}
mu[i * pri[k]] = - mu[i];
phi[i * pri[k]] = (pri[k] - 1) * phi[i];
}
}
for (int i = 1; i <= n; ++ i) f[i] = 1ll * i * i % MOD * phi[i];
for (int i = 1; i <= n; ++ i) f[i] = (f[i - 1] + f[i]) % MOD;
}
int Q, N;
namespace BIT {
int T[MAXN];
#define lowbit(x) (x & -x)
inline void Add (const int x, const int v) {
for (int i = x; i <= N; i += lowbit (i)) T[i] = (T[i] + v) % MOD;
}
inline void Mns (const int x, const int v) {
for (int i = x; i <= N; i += lowbit (i)) T[i] = (T[i] - v + MOD) % MOD;
}
inline int Sum (const int x) {
int ans = 0;
for (int i = x; i; i -= lowbit (i)) ans = (ans + T[i]) % MOD;
return ans;
}
}
int64_t val[MAXN];
inline void Update (const int x, const int y, const int64_t v) {
int d = std::__gcd (x, y);
BIT::Mns (d, val[d] % MOD), val[d] = v / (1ll * (x / d) * (y / d)), BIT::Add (d, val[d] % MOD);
}
inline int Solve (const int k) {
int ans = 0, d = 0;
for (int l = 1, r; l <= k; l = r + 1) {
r = k / (k / l), d = k / l;
ans = (ans + 1ll * (BIT::Sum (r) - BIT::Sum (l - 1) + MOD) % MOD * f[d] % MOD) % MOD;
}
return ans;
}
int a, b, k;
int64_t c;
int main () {
std::cin >> Q >> N, Sieve ();
for (int i = 1; i <= N; ++ i) Update (i, i, 1ll * i * i);
for (int i = 1; i <= Q; ++ i) {
std::cin >> a >> b >> c >> k, Update (a, b, c);
std::cout << Solve (k) << '\n';
}
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具