[题解]calc 的两种解法
前言
神 \(\mathrm{{\color{black}w}{\color{red}{ind\_whisper}}}\) 爆切拉格朗日插值之后忽悠我做这个题.
但是我太菜了现在才稍微懂一点.
解法 1
首先顺序可以忽略,最后乘上 \(n!\) 即可.
然后可以设计朴素 DP :
令 \(f_{i,j}\) 表示前 \(j\) 个数里已经选出 \(i\) 个的全部方案权值和.
写出 \(\mathcal{O} (nk)\) 的转移方程 :
\(f_{i,j} = j \times f_{i - 1,j - 1} + f_{i,j - 1}\)
想个办法将求这个的过程变成 \(\mathcal{O} (n^2)\) 的.
首先一直求到 \(f_{n,n}\) 是 \(n^2\) 的,将其定义为一个函数 : \(F(x) = f_{n,x}\),那么如果其是一个多项式,就能仅仅靠连续求出的前几项进行拉格朗日插值求出结果.
现在尝试证明其为一个次数 \(\mathcal{O}(n)\) 级别的多项式.
将原 DP 状态差分 : \(g_{i,j} = f_{i,j} - f_{i,j - 1}\)
可以发现这时舍弃了 \(f_{i,j}\) 中不含有数 \(j\) 的方案,即 \(g_{i,j}\) 表示前 \(j\) 个数且包含 \(j\) 选出了 \(i\) 个数的全部方案权值总和.
然后写出 \(g\) 转移方程 :
首先对 \(g_{i - 1}\) 求了前缀和,次数升高一次,再乘 \(j\) 次数再升高一次,共升高两次.
于是 \(g_{n}\) 是 \(2n\) 次的.
于是 \(f_{n}\) 作为 \(g_{n}\) 的前缀和,是 \(2n + 1\) 次的.
暴力 DP 然后拉格朗日插值即可.
Code :
int n,k,p;
ll f[N][N << 1];
ll fac[N << 2],ifac[N << 2];
ll pre[N << 2],suf[N << 2];
ll lagrange(ll x,ll k) {
k %= p;
fac[0] = 1;rep(i,1,x) fac[i] = fac[i - 1] * i % p;
ifac[x] = qpow(fac[x],p - 2,p);
repb(i,x,1) ifac[i - 1] = ifac[i] * i % p;
pre[0] = 1,suf[x + 1] = 1;
rep(i,1,x) pre[i] = pre[i - 1] * (ll)(k - i) % p;
repb(i,x,1) suf[i] = suf[i + 1] * (ll)(k - i) % p;
ll res = 0;
rep(i,1,x) {
ll tmp = f[n][i] * pre[i - 1] % p * suf[i + 1] % p * ifac[i - 1] % p * ifac[x - i] % p;
if((x - i) & 1) tmp = p - tmp;
res = (res + tmp) % p;
}
return res;
}
int main() {
init_IO();
k = read(),n = read(),p = read();
rep(i,0,(n << 1 | 1)) f[0][i] = 1;
rep(i,1,n) rep(j,1,(n << 1 | 1))
f[i][j] = (f[i][j - 1] + f[i - 1][j - 1] * (ll)j) % p;
ll ans = lagrange((n << 1 | 1),k);
write(ans * fac[n] % p),enter;
end_IO();
return 0;
}
解法 2
依然把顺序忽略,最后乘上 \(n!\)
首先这个组合选数然后合在一起是一个每个数单个求 \(\mathbf{OGF}\) 封闭形式是很简单的,那么总的结果就是每个 \(\mathbf{OGF}\) 封闭形式卷积 :
但是 \(\prod\) 可比 \(\sum\) 可怕太多了,考虑如何让乘法变成加法,对每个取单个数的 \(\mathbf{OGF}\) 取 \(\ln\) 然后加起来最后 \(\mathrm{exp}\).
也就是求 : \(\ln (1 + kx)\) 化成一个级数的形式.
首先可以对 \(\ln\) 求导然后积分回来,然后得到等量关系 :
对于 \(\dfrac{x}{1 + kx}\) 这样的分式可以将其看作等比数列求和的结果来展开 :
然后众所周知和式和积分都是求和,用和式的交换求和次序技巧 :
发现里面的部分的积分很好求,把积分干掉换成分式 :
然后看着 \(i + 1\) 很别扭,稍微改写一下 :
然后这是对于一个数取数情况贡献的 \(\mathbf{OGF}\),再看一眼原式 :
其中标红部分已经可以用上面求出的无限和式来替换,然后再对 \(F(x)\) 求 \(\ln\) 把 \(\prod\) 变成 \(\sum\) :
再交换一次求和次序 :
所以就是求 \(\sum_{i = 1}^{k} i^j\) 比较重要.
如果只是普通版那就可以参考 这道题 使用拉格朗日插值即可计算,后面 \(\mathrm{exp}\) 的部分就交给任意模数多项式乘法/多项式乘法逆了.
现在来看加强版,模数是 \(998244353\),非常良心.
但是搜遍全网会发现自然数幂和通常说的都是这样一个玩意 :
然后无论是斯特林数还是伯努利数还是多一个 \(\log\) 的分治 FFT 都是在求同一 \(k\) 不同 \(n\) 的情况,现在求的是同一 \(n\) 不同 \(k\) 的情况.
怎么说呢,就很像第二类斯特林数行/列之间的关系.
那还是直接写生成函数吧.
写出 \(\left< S_0(k),S_1(k),S_2(k),\cdots \right>\) 这样一个数列的 \(\mathbf{EGF}\).
然后众所周知泰勒展开有一个非常有名的例子就是 把 \(e^x\) 在 \(x = 0\) 展开 :
于是上式化为 :
是一个等比数列,套求和公式.
然后发现不满足零次项为 \(1\) ,但是零次项分母也是 \(0\),同时除以 \(x\),一口把那一项吞了.
成功将复杂度降低到 \(\mathcal{O} (n\log n)\)
Code :
PolyInv
,PolyMul
,PolyExp
参见 这里
inline void init_calc(int n) {
fac[0] = 1;
rep(i,1,n) fac[i] = (ll)fac[i - 1] * i % MOD;
ifac[n] = qpow(fac[n]);
repb(i,n,1) ifac[i - 1] = (ll)ifac[i] * i % MOD;
}
int main() {
init_IO();
init_root();
int k = read(),m = read() + 2;
init_calc(m);
int pw = k;
repl(i,0,m) {
F[i] = (ll)pw * ifac[i + 1] % MOD;
pw = (ll)pw * k % MOD;
}
repl(i,0,m)
G[i] = ((i + 1) & 1) ? ifac[i + 1] : MOD - ifac[i + 1];
PolyInv(G,H,m);
PolyMul(F,H,F,m,m);
mems(G,0);
repl(i,1,m) {
if(i & 1) G[i] = fac[i - 1];
else G[i] = MOD - fac[i - 1];
G[i] = ((ll)G[i] * F[i]) % MOD;
}
mems(F,0);
PolyExp(G,F,m);
repl(i,1,m - 1)
write((ll)F[i] * fac[i] % MOD),enter;
end_IO();
return 0;
}