【2020省选Day1T2】 LOJ3300 「联合省选 2020 A」组合数问题
解法一:递推求导,搞定k^i
前置知识
求导法则
基本法则:\((x^k)'=kx^{k-1}\)
四则运算:
- \((f(x)+g(x))'=f'(x)+g'(x)\)
- \((f(x)\cdot g(x))'=f'(x)g(x)+f(x)g'(x)\)
用乘法法则,可以推出一个常数乘以一个函数的求导法则,即:\((c\cdot f(x))'=0\cdot f(x)+c\cdot f'(x)=c\cdot f'(x)\)。然后,对于减法,就可以看做加\(-1\cdot g(x)\),直接套用加法法则即可得到:\((f(x)-g(x))'=f'(x)-g'(x)\)。
复合函数:\((f(g(x)))'=f'(g(x))\cdot g'(x)\)。
对于除法,可以看做乘以\(\frac{1}{g(x)}\),也就是\(h(x)=x^{-1}\)和\(g(x)\)的复合函数。于是得到:\(\displaystyle \left(\frac{f(x)}{g(x)}\right)'=f'(x)\cdot\frac{1}{g(x)}+f(x)\cdot(-g^{-2}(x)\cdot g'(x))=\frac{f'(x)g(x)-f(x)g'(x)}{g^2(x)}\)。
应用:对一个多项式函数,\(F(x)=\sum_{i=0}^{n}f_ix^i\)求导,可以结合“基本法则”和“加法法则”,得到:\(F'(x)=\sum_{i=0}^{n}f_{i}\cdot i\cdot x^{i-1}=\sum_{i=0}^{n-1}f_{i+1}(i+1)x^i\)。
二项式定理
证明略。
本题用到它的一种特殊形式,就是\((x+1)^{n}=\sum_{i=0}^{n}{n\choose i}x^i\)。你可以理解为\(y=1\)的情况。
题解
考虑\(m=0\)的subtask,答案就是:\(a_0\cdot \sum_{k=0}^{n}x^k{n\choose k}\)。容易发现这就是上面讲的“二项式定理的特殊形式”,所以它等于\(a_0\cdot(x+1)^n\)。
这给我们一个启发,就是可以把\(f(k)\)的每一项拿出来分别计算。于是原式就等于:
我们设\(F_i(x)=\sum_{k=0}^{n}k^ix^k{n\choose k}\)。那么原式就等于\(\sum_{i=0}^{m}a_iF_i(x)\)。发现\(F_i(x)\)里,比较令人头疼的,就是这个\(k^i\)。怎么处理它呢?本节介绍的“求导”,就是一种很好的方法。
对一个多项式函数,\(T(x)=\sum_{k=0}^{n}t_kx^k\),考虑把它每一项分别都乘以\(k\),得到\(\text{newT}(x)=\sum_{k=0}^{n}t_k\cdot k\cdot x^k\)。发现这个是什么呢?这个就是把\(T'(x)\)每项都乘以\(x\)。也就是说:\(\text{newT}(x)=x\cdot T'(x)\)。
回到本题,我们知道,\(F_i(x)\),就相当于把\(F_{i-1}(x)\)的每一项分别都乘以\(k\)。所以,\(F_i(x)=x\cdot F_{i-1}'(x)\)。也就是先求导,再乘以\(x\)。例如,\(F_0(x)=(x+1)^{n}\),那么就有:\(F_1(x)=x((x+1)^n)'\)。带到原式里,也可以验证出来(把每一项都提一个\(x\)出来即可)。
那么我们就可以依次递推\(F_1(x),F_2(x),\dots ,F_m(x)\)。也就是先对\(F_{i-1}(x)\)求导,再把所有项都乘以\(x\)。例如我多写几个:
- \(F_0(x)=(x+1)^{n}\)
- \(F_1(x)=x((x+1)^n)'=n\cdot x\cdot (x+1)^{n-1}\)
- \(F_2(x)=x(n\cdot x\cdot (x+1)^{n-1})'=n\cdot x\cdot (x+1)^{n-1}+n(n-1)\cdot x^2\cdot (x+1)^{n-2}\)
- $\dots $
你可以发现,它有很多项。每一项,都可以写成\(d_j\cdot x^{j}\cdot (x+1)^{n-j}\)的形式。其中,\(d_j\)是一个常数。\(0\leq j\leq m\)。更具体来说:
这个\(d\)数组,我们递推一下,可以\(O(m^2)\)求出。然后预处理一下\(x\)和\((x+1)\)的次幂,就可以直接计算答案了。
\(d\)数组怎么递推?首先根据导数的加法法则,我们要对\(F_{i-1}(x)\)求导,就先对它的每一项求导,再加起来。考虑某一项\(d_{i-1,j}x^{j}(x+1)^{n-j}\),它是一个乘法(前面的常数不算,是\(x^j\)和\((x+1)^{n-j}\)两个东西相乘)。要对它求导,就用到导数的乘法法则:\((f(x)\cdot g(x))'=f'(x)g(x)+f(x)g'(x)\)。所以\(F_{i-1}(x)\)里的每一项,会对\(F_{i}(x)\)里的两项有贡献。当然,别忘了对\(F_{i-1}(x)\)求导后,还要把结果再乘以\(x\)。具体地也可以写成:
时间复杂度\(O(m^2)\)。
参考代码(在LOJ查看):
//problem:LOJ3300
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int,int> pii;
const int MAXM=1000;
int MOD;
inline int mod1(int x){return x<MOD?x:x-MOD;}
inline int mod2(int x){return x<0?x+MOD:x;}
inline void add(int& x,int y){x=mod1(x+y);}
inline void sub(int& x,int y){x=mod2(x-y);}
inline int pow_mod(int x,int i){int y=1;while(i){if(i&1)y=(ll)y*x%MOD;x=(ll)x*x%MOD;i>>=1;}return y;}
int n,x,m,a[MAXM+5],d[MAXM+5][MAXM+5],pow1[MAXM+5],pow2[MAXM+5];
int main() {
cin>>n>>x>>MOD>>m;x%=MOD;
for(int i=0;i<=m;++i)cin>>a[i],a[i]%=MOD;
d[0][0]=1;
for(int i=1;i<=m;++i){
for(int j=0;j<=m;++j){
// d[i][j] * x^{j} * (x+1)^{n-j}
d[i][j]=(ll)d[i-1][j]*j%MOD;
if(j>0)add(d[i][j],(ll)d[i-1][j-1]*(n-(j-1))%MOD);
}
}
pow1[0]=pow_mod(x+1,n-m);
for(int i=1;i<=m;++i)pow1[i]=(ll)pow1[i-1]*(x+1)%MOD;
pow2[0]=1;
for(int i=1;i<=m;++i)pow2[i]=(ll)pow2[i-1]*x%MOD;
int ans=0;
for(int i=0;i<=m;++i){
for(int j=0;j<=m;++j){
add(ans,(ll)a[i]*d[i][j]%MOD*pow2[j]%MOD*pow1[m-j]%MOD);
}
}
cout<<ans<<endl;
return 0;
}
解法二:用组合恒等式,拆出斯特林数
前置知识
一个组合恒等式
证明:
把两边按定义展开:
左边\(\displaystyle=k\cdot \frac{n!}{k!(n-k)!}\)。
右边\(\displaystyle=n\cdot \frac{(n-1)!}{(k-1)!(n-k)!}\)。
两边同时乘以\(\frac{1}{k}\),都等于\(\frac{n!}{k!(n-k)!}\)。
所以左边\(=\)右边。
下降幂的定义
题解
根据上一种方法的初步转化,原式可以写成:
设\(F_i(x)=\sum_{k=0}^{n}k^ix^k{n\choose k}\)。那么原式就等于\(\sum_{i=0}^{m}a_iF_i(x)\)。还是考虑求\(F_i(x)\)。我们的重点是要把\(k^i\)消掉,剩余的部分,如果是一个\(x^k\)再乘一个组合数,可以直接套二项式定理。因为有了前面的那个组合恒等式,所以现在思路比较明确:先保留\(x^k\)不要动,拿组合数去和\(k^i\)消,消出一个新的组合数加一个系数,我们就赢了。
我们先考虑\(i\geq1\)的情况。
当\(i=1\)时,用我们前面写的组合恒等式,可以写成:
当\(i=2\)时,也可以推:
其中,最重要的一步,是我们把一个\(k\)拆成了\((1+(k-1))\)。这很巧妙。其他的地方都是在套那个组合恒等式。
看最后推出的结果,发现,每个组合数前,是一个\(n\)的下降幂。所以我们盲猜:\(F_i(x)=\sum_{k=0}^{n}x^k\sum_{j=1}^{i}n^{\underline{j}}{n-j\choose k-j}\)。
很遗憾,这个猜想不对。
事实上,在\(n^{\underline{j}}{n-j\choose k-j}\)前,还需要加一些系数。只不过对于\(F_1(x)\)和\(F_2(x)\)来说,这个系数恰好都是\(1\)。
我们继续考虑\(i=3\)的情况。
ps:因为公式太大搞成图片了,如果没加载出来,就自己点一下链接吧。https://cdn.luogu.com.cn/upload/image_hosting/fg2qsqu3.png
发现没有,现在,后面的系数变成了\(1\ 3 \ 1\)。不全是\(1\)了。
容易发现,\(F_i\)后面的这一坨东西,恰好有\(i\)项。如果设第\(j\)项前面的系数为\(s(i,j)\),则可以写成:
考虑如何求这个\(s_{i,j}\)。首先,\(F_{i-1}(x)\)后面的东西里,每一项都要乘以\(k\)。例如,\(s_{i-1,j}\cdot n^{\underline{j}}{n-j\choose k-j}\),乘以\(k\),变成\(k\cdot s_{i-1,j}\cdot n^{\underline{j}}{n-j\choose k-j}\)。然后,我们会把\(k\),拆成\((j+(k-j))\)。所以\(s_{i-1,j}\)会对\(s_{i,?}\)里的两个地方产生贡献。一个是\(j\)产生的,贡献到\(s_{i,j}\),贡献系数为\(j\)。另一个是\((k-j)\)产生的,贡献到\(s_{i,j+1}\),贡献系数为\(1\)。
那反过来说,就可以得到:
其中,边界是\(s_{1,1}=1\)。
于是就可以\(O(m^2)\)递推求出整个\(s\)数组。顺便说一句,这里的\(s\),其实就是第二类斯特林数,不过有没有看出来都不影响解题。
现在求出了\(s\),再回头看\(F_i(x)\)。我们按一开始说的思路,用二项式定理,把\(x^k\)和组合数搞到一起去。具体来说:
以上是\(i\geq 1\)的情况。当\(i=0\)时,显然有\(F_0=(x+1)^n\)。当然,你也可以认为,\(s_{0,0}=1\),\(n^{\underline{0}}=1\),\(s_{i,0}=0\) (\(i\geq 1\))。然后,你把\(j\)改成从\(0\)开始循环,也是对的。这样就不用特判\(i=0\)的情况。
然后答案就是\(\sum_{i=0}^{m}a_iF_i(x)\)。
时间复杂度\(O(m^2)\)。
参考代码,他鸽了。
两种解法的比较与联系
事实上,推到最后,大家都能看出来,第一种解法里的\(d_{i,j}\),就是第二种解法里的\(s_{i,j}\cdot n^{\underline{j}}\)。
两种解法,都想消掉\(k^i\)。他们走的路径不同。
- 第一种解法,比较鲁莽。你不是有一个\(k^i\)吗,我对着你硬消。求一次导干掉一个\(k\)。这种“鲁莽”的背后,是有“求导”这个强大的工具在支持。
- 第二种解法,看起来稍微圆滑一点。我先让组合数\({n\choose k}\)去和你消。用的就是一个组合恒等式。然后等你\(k^i\)没了,我再把剩下的组合数和\(x^k\)打包成一个二项式定理。