题解:P13541 [OOI 2022] Good arrays

· · 题解

题意描述

一个正整数数组是好数组,当且仅当每个数都是上一个数的因数。

求长度为 n 且每个数都不超过 c 的好数组个数。

## 动态规划 考虑不断向好数组的末尾添加新数。设 $f_{i,j}$ 表示长度为 $i$,且最小数(即末尾)为 $j$ 的好数组个数。则转移方程: $$f_{i-1,j\times k}\to f_{i,j}\quad(j\times k\le c)$$ 时间复杂度 $O(nc\log c)$,可以获得 $41$ 分。 ```cpp #include<bits/stdc++.h> using namespace std; #define ll long long int n,c; ll f[5010][5010],ans; const ll P=998244353; int main(){ scanf("%d%d",&n,&c); for(int i=1;i<=c;i++) f[1][i]=1; for(int i=2;i<=n;i++) for(int j=1;j<=c;j++) for(int k=1;j*k<=c;k++) (f[i][j]+=f[i-1][j*k])%=P; for(int i=1;i<=c;i++) (ans+=f[n][i])%=P; printf("%lld",ans); } ``` ### 拉格朗日插值 考场上想到的神秘优化。每一轮更新时每个 $f_j$ 都从特定节点转移而来。转移深度每加一层,关于 $i$ 的多项式 $h(i)=f_{i,j}$ 就会加一次。 由于转移最多 $O(\log c)$ 层,题目所求的 $g(i)=\sum\limits_{j=1}^cf_{i,j}$ 为 $\lfloor\log c\rfloor$ 次多项式,所以拉格朗日插值大力求出 $g(n)$ 即可。 时间复杂度 $O(c\log^2c)$,可以获得 $57$ 分。 ```cpp #include<bits/stdc++.h> using namespace std; #define ll long long int n,c,b; ll f[20][100010],g[20],ans; const ll P=998244353; ll qpw(ll a,ll b){ ll res=1; while(b){ if(b&1) (res*=a)%=P; (a*=a)%=P; b>>=1; } return res; } int main(){ scanf("%d%d",&n,&c); b=__lg(c)+1; for(int i=1;i<=c;i++) f[1][i]=1; g[1]=c; for(int i=2;i<=b;i++) for(int j=1;j<=c;j++){ for(int k=1;j*k<=c;k++) (f[i][j]+=f[i-1][j*k])%=P; (g[i]+=f[i][j])%=P; } for(int i=1;i<=b;i++){ ll res=g[i]; for(int j=1;j<=b;j++) if(i!=j) res=res*(n-j+P)%P*qpw(i-j+P,P-2)%P; (ans+=res)%=P; } printf("%lld",ans); } ``` ## 组合数学 DP 没什么前途,开始推式子。 设长度为 $n$ 的好数组为 $a$,“差分数组”为 $b_i=\frac{a_i}{a_{i+1}}\,(1\le i<n)$。一个 $a_1$ 和差分数组唯一对应一个好数组。因此我们只需求出差分数组的个数即可。我们发现 $\prod\limits_{i=1}^{n-1}b_i=\frac{a_1}{a_n}$。所以设 $f(x)$ 为 $x=\frac{a_1}{a_n}$ 时差分数组的个数,即把 $x$ 分解成有序的 $n-1$ 个数之积的方案数。 设 $x=\prod p_i^{\alpha_i}$($p_i$ 是质数),则每个质因子都能分到 $n-1$ 个数中,插板法 $+$ 乘法原理可得 $f(x)=\prod\binom{\alpha_i+n-2}{n-2}$。 答案为 $\sum\limits_{i=1}^c\sum\limits_{i\times j\le c} f(i)=\sum\limits_{i=1}^c\lfloor\frac{c}{i}\rfloor f(i)$。 时间复杂度 $O(c\sqrt c)$,可以获得 $57$ 分。 ```cpp #include<bits/stdc++.h> using namespace std; #define ll long long #define N 100020 int n,c; ll fac[N][2],f[N],ans; const ll P=998244353; ll qpw(ll a,ll b){ ll res=1; while(b){ if(b&1) (res*=a)%=P; (a*=a)%=P; b>>=1; } return res; } ll C(ll n,ll m){ return fac[n][0]*fac[m][1]%P*fac[n-m][1]%P; } ll calc(int x){ ll res=1; for(int i=2;i*i<=x;i++){ int cnt=0; while(x%i==0){ cnt++; x/=i; } (res*=C(cnt+n-2,n-2))%=P; } if(x!=1) (res*=C(1+n-2,n-2))%=P; return res; } int main(){ fac[0][0]=1; for(int i=1;i<N;i++) fac[i][0]=fac[i-1][0]*i%P; fac[N-1][1]=qpw(fac[N-1][0],P-2); for(int i=N-1;i>=1;i--) fac[i-1][1]=fac[i][1]*i%P; scanf("%d%d",&n,&c); for(int i=1;i<=c;i++){ f[i]=calc(i); (ans+=c/i*f[i]%P)%=P; } printf("%lld",ans); } ``` ### 欧拉筛优化 我们发现瓶颈在于枚举质因子,考虑一边线性筛一边转移。 我们知道,线性筛中每个合数 $k$ 会被 $i\times p_j$ 筛掉,其中 $p_j$ 是 $k$ 的最小质因子。由于我们要计算的 $\binom{\alpha_i+n-2}{n-2}$ 内含有 $\alpha_i$,所以我们记录 $g_k$ 表示 $k$ 的最小质因子的指数。 显然若 $i$ 为质数,$f_i=\binom{1+n-2}{n-2}$。考虑转移。 1. $i\bmod p_j=0$:这说明 $i$ 中已经含有质因子 $p_j$,现在又加一次。 $$f_i\times\frac{\binom{g_i+1+n-2}{n-2}}{\binom{g_i+n-2}{n-2}}\to f_k$$ $$g_i+1\to g_k$$ 2. $i\bmod p_j\neq0$:这说明 $k$ 中含有 $i$ 所不含的更小的质因子 $p_j$。 $$f_i\times\binom{1+n-2}{n-2}\to f_k$$ $$1\to g_k$$ 特别地,$f_1=1$。 时间复杂度 $O(n+c)$,可以得到 $100$……补兑!怎么 MLE/TLE 了? ```cpp #include<bits/stdc++.h> using namespace std; #define ll long long #define N 50000030 int n,c,prime[N/10],cnt,g[N]; bool flag[N]; ll fac[N][2],f[N],ans; const ll P=998244353; ll qpw(ll a,ll b){ ll res=1; while(b){ if(b&1) (res*=a)%=P; (a*=a)%=P; b>>=1; } return res; } ll C(ll n,ll m){ return fac[n][0]*fac[m][1]%P*fac[n-m][1]%P; } ll invC(ll n,ll m){ return fac[n][1]*fac[m][0]%P*fac[n-m][0]%P; } int main(){ fac[0][0]=1; for(int i=1;i<N;i++) fac[i][0]=fac[i-1][0]*i%P; fac[N-1][1]=qpw(fac[N-1][0],P-2); for(int i=N-1;i>=1;i--) fac[i-1][1]=fac[i][1]*i%P; scanf("%d%d",&n,&c); f[1]=1; for(int i=2;i<=c;i++){ if(!flag[i]){ prime[++cnt]=i; f[i]=C(1+n-2,n-2); g[i]=1; } for(int j=1;j<=cnt&&i*prime[j]<=c;j++){ int k=i*prime[j]; flag[k]=1; if(i%prime[j]==0){ f[k]=f[i]*invC(g[i]+n-2,n-2)%P*C(g[i]+1+n-2,n-2)%P; g[k]=g[i]+1; break; }else{ f[k]=f[i]*C(1+n-2,n-2)%P; g[k]=1; } } } for(int i=1;i<=c;i++) (ans+=c/i*f[i]%P)%=P; printf("%lld",ans); } ``` ### 卡常 针对 MLE: * 变量/数组定义为 `int`,需要运算时临时转换为 `long long`。 针对 TLE: * 把全局变量/数组定义在 `main()` 内,因为栈空间读写速度比堆空间快。 * 预处理 $h(x)=\binom{x+n-2}{n-2},invh(x)=\frac{1}{h(x)}$。 现在可以获得 $100$ 分。 ```cpp #include<bits/stdc++.h> using namespace std; #define N 50000030 const int P=998244353; int qpw(int a,int b){ int res=1; while(b){ if(b&1) res=1ll*res*a%P; a=1ll*a*a%P; b>>=1; } return res; } int C(int fac[][2],int n,int m){ return 1ll*fac[n][0]*fac[m][1]%P*fac[n-m][1]%P; } int invC(int fac[][2],int n,int m){ return 1ll*fac[n][1]*fac[m][0]%P*fac[n-m][0]%P; } int main(){ int n,c,prime[N/10],cnt=0,fac[N][2],f[N],g[N],h[30],invh[30],ans=0; bool flag[N]; fac[0][0]=1; for(int i=1;i<N;i++) fac[i][0]=1ll*fac[i-1][0]*i%P; fac[N-1][1]=qpw(fac[N-1][0],P-2); for(int i=N-1;i>=1;i--) fac[i-1][1]=1ll*fac[i][1]*i%P; memset(flag,0,sizeof(flag)); scanf("%d%d",&n,&c); for(int i=1;i<30;i++){ h[i]=C(fac,i+n-2,n-2); invh[i]=invC(fac,i+n-2,n-2); } f[1]=1; for(int i=2;i<=c;i++){ if(!flag[i]){ prime[++cnt]=i; f[i]=h[1]; g[i]=1; } for(int j=1;j<=cnt&&i*prime[j]<=c;j++){ int k=i*prime[j]; flag[k]=1; if(i%prime[j]==0){ f[k]=1ll*f[i]*invh[g[i]]%P*h[g[i]+1]%P; g[k]=g[i]+1; break; }else{ f[k]=1ll*f[i]*h[1]%P; g[k]=1; } } } for(int i=1;i<=c;i++) ans=(ans+1ll*c/i*f[i])%P; printf("%d",ans); } ```