题解 2021 年 ICPC 昆明赛区 E. Counting Binary Trees
考虑记根节点标号恰为 \(n\) 的方案数为 \(f(n)\),要求的就是
\[\begin{aligned}
F(n)&=\sum_{i=1}^n f(i)
\end{aligned}
\]
而题目的条件相当于 \(f=f\otimes f+p\),其中 \(p(n)=[\exists k_i\mid n]\)。注意到 \(f(1)=0\),于是这确实是良定义的。
然后把前缀和拆一下,就有
\[\begin{aligned}
F(n)=&P(n)+\sum_{i=1}^n(f\otimes f)(i)\\
=&P(n)+\sum_{i,j\geq 1, ij\leq n}f(i)f(j)\\
=&P(n)+\sum_{i=1}^nf(i)F(n/i)
\end{aligned}
\]
这是可以直接递推的。具体来说,可以类似杜教筛地预处理前 \(\mathcal O(n^{2/3})\) 的点值,然后后面的递归下去算,平衡一下块大小就能过。
#include<cstdio>
#include<cstring>
#include<cmath>
#include<vector>
#include<unordered_map>
#include<algorithm>
typedef long long ll;
const ll mod = 998244353;
const int maxn = 1E+6 + 5;
int T, n, m, N, k[10];
ll F[maxn];
inline bool p(int n) {
for(int i = 0; i < m; ++i)
if(n % k[i] == 0) return true;
return false;
}
inline int sgn(int x) { return x & 1 ? -1 : 1; }
int LCM[100];
inline int lcm(int x, int y) { return x * y / std::__gcd(x, y);}
inline int P(int n) {
int res = 0;
for(int S = 1; S < (1 << m); ++S) {
int ppcnt = 0;
res += sgn(__builtin_popcount(S) + 1) * (n / LCM[S]);
} return res;
}
std::vector<int> d[maxn];
inline void PRE(int N) {
for(int d = 1; d <= N; ++d)
for(int k = 1; k * d <= N; ++k)
::d[d * k].push_back(d);
}
inline void pre(int N) {
for(int i = 2; i <= N; ++i) {
F[i] = p(i);
for(int d : ::d[i])
(F[i] += F[d] * F[i / d]) %= mod;
}
for(int i = 2; i <= N; ++i) (F[i] += F[i - 1]) %= mod;
}
std::unordered_map<int, ll> M;
inline ll sF(int n) {
if(n <= N) return F[n];
if(M.count(n)) return M[n];
ll res = P(n);
for(int l = 2, r; l <= n; l = r + 1) {
r = n / (n / l); if(r == n) break;
(res += (sF(r) - sF(l - 1)) * sF(n / l)) %= mod;
}
return M[n] = res;
}
int main() {
scanf("%d", &T), PRE(6E+5);
while(T --> 0) {
scanf("%d%d", &n, &m), M.clear();
for(int i = 0; i < m; ++i) scanf("%d", &k[i]);
for(int S = 1; S < (1 << m); ++S) {
LCM[S] = 1;
for(int i = 0; i < m; ++i)
if(S >> i & 1) LCM[S] = lcm(LCM[S], k[i]);
}
pre(N = pow(n, 0.666) / 2 + 50), printf("%lld\n", (sF(n) + mod) % mod);
}
}