生成函数
普通生成函数(OGF)
定义
生成函数将数列转化成函数的形式处理。数列中的每一个数依次对应函数中的系数。具体而言,有
比较显而易见的特点是,如果系数所对应的序列有通项公式,则系数就是通项公式,如
对于 \(a_i=i^2\),即 \(a=<1,4,9,16\dots >\),生成函数就是 \(\sum_{i=0}^n=i^2x^i\)
基本运算
事实上,运算时我们就将生成函数视为普通函数运算。
设函数 \(F,G\) 的系数分别为 \(a_i,b_i\)
加减法直接对位相加
乘法就是普通卷积
封闭形式
听起来有些神奇,我们可以将一个无限的生成函数转化为封闭形式。最经典的是:
观察一下显然有
解方程立得
通过类似的推导过程我们有
这两个式子在生成函数转封闭形式的时候非常常用。
将封闭形式转化成通项公式的常规方法是待定系数法。如
我们将其拆分为和的形式,形如
显然这几项可以通分变为原式。直接解方程即可(直接通分后拆开括号对应系数)。
再求对应的生成函数的展开式的系数,加起来就是生成函数的通项公式。通用的解法是广义二项式定理展开。
广义二项式定理:
展开后就很好做了。注意展开前先满足广义二项式定理的形式。
如果生成函数的系数是递推式,那么我们一般也可以将其转化为封闭形式,比如斐波那契数列:
我们求这个序列的生成函数 \(F(x)\)。
一般的方法是借助其系数的可加性。
第二式与第三式相加与第一式联立有
将 \(f_1,f_0\) 带入后立有
这个东西也很常用。
P10780 BZOJ3028 食物
非常经典的例题。
- 将每种食物能选设做1,选不了设做0,发现对应的生成函数的乘积就是方案数。将所有食物乘起来对应的系数就是总方案数。
乘出来生成函数是
同样是二项式定理
对 \((1 - x)^{-4}\) 应用广义二项式定理:
广义二项式系数 \(\binom{-4}{n}\) 为:
因此,展开式为:
将生成函数乘以 \(x\):
通过改变索引 \(m = n + 1\),我们得到:
因此,生成函数 \(\frac{x}{(x-1)^4}\) 的通项公式为:
对于 \(m \geq 1\),我们可以将其表示为:
因此
生成函数 \(\frac{x}{(x-1)^4}\) 的通项公式为:
答案就是这个了。
广义二项式定理的另一种形式
这种形式要常用的多。
证明就可以用广义二项式定理来证明。
P4451 [国家集训队] 整数的lqp拆分
注意到有一个显然的 DP:
设 \(f_{i,j}\) 表示到第 \(i\) 个数,\(a\) 数组的和为 \(j\) 的总贡献。有转移
其中 \(F_k\) 表示斐波那契数列的第 \(k\) 个数。最后的答案就是 \(\sum_if_{i,n}\)。
注意到上面的形式就是一个卷积。我们同样设 \(F(x)\) 表示斐波那契数列的生成函数,那答案等价于
注意到当 \(i>n\) 的时候 \(F^i(x)\) 的第 \(n\) 项系数为 0(从生成函数的意义上讲,这等价于“不存在一种方案使得存在 \(i>n\) 个正整数的和恰好为 \(n\)”),因此等价于
发现这个就是最正常的生成函数的形式,转成封闭形式答案同样等价于
考虑斐波那契数列的封闭形式 \(F(x)=\frac{x}{1-x-x^2}\),将其带入有
由于我们要求的是第 \(n\) 项的系数,那个常数项 1 只影响 \(n=0\) 时的答案,因此我们直接抛弃到最后特判掉这种情况即可。于是现在的答案变成
发现分子乘上一个 \(x\) 实际上等价于对生成函数的平移,因此我们可以先考虑分母的系数再平移一下即可。
于是现在要求的变成了
这里需要使用一种称为“特征方程”的技巧将封闭形式变成一般形式。(虽然不知道这个名字的含义)
这种技巧实际上貌似包含了很多种处理方式,我使用了一种个人相对好理解的方式。
首先是将分母因式分解:
其中 \(x_1=\sqrt2-1,x_2=-\sqrt2-1\)。因式分解的目的是化出下面的式子:
注意加的顺序和符号,变化了一下。
可以发现这个东西是对的。这个就是这种方法的精华所在,也就是化出所谓 \(\frac{1}{1-x}\) 的形式。注意到提出来 \(\frac{1}{x_2 - x_1}\) 的系数是巧妙的,需要掌握一下。
然后我们之前去掉了一个 \(x\) 的系数,添加上了以后答案变成
于是第 \(n\) 项的系数变成了
发现要求 \(\sqrt2\),于是要二次剩余,于是看题解发现 \(\sqrt2 \equiv 59713600 \pmod{10^9+7}\)。
code
注意到 \(n\) 在指数上,因此对 \(n\) 取模根据扩展欧拉定理应该是 \(10^9+8\)。因为这个写了 1 分钟调了半小时。/ll
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int p=1e9+7,ni=59713600;
int ksm(int x,int k){
int res=1;
while(k){
if(k&1)res=res*x%p;
x=x*x%p,k>>=1;
}
return res;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int n=0;char s;
while(cin>>s){
n=(n*10+(s-'0'))%(p-1);//注意这里 n 在指数上,要模 p-1
}
if(n==0ll){cout<<'1';return 0;}
cout<<1ll*ni/4ll*((ksm(1ll+ni,n)-ksm(1ll-ni+p,n)+p)%p)%p;return 0;
}
P6078 [CEOI 2004] Sweets
同样是入门的推式子题,但是搞得我比较烦。
考虑每个罐子的生成函数。由于每个罐子吃特定颗糖只有一种本质不同的情况,因此每一项系数都是 1。具体的,对于第 \(i\) 个罐子,其吃糖的方案数的生成函数为
然后合起来的方案数就是
发现有可差分性,于是差分一下变成求前缀。然后考虑转化一下式子。
发现 \(F_i(x)\) 的形式是相对经典的形式,我们同样通过用 \(F_i(x)\) 和 \(xF_i(x)\) 联立的方法求出 \(F_i(x)\) 的封闭形式,有
设 \(G(x)=\prod_{i=1}^n F_i(x)\),那么带入有
由于 \(n\) 非常小,我们完全可以通过暴力多项式乘法将分子上的式子展开成为一般形式。我们设这个分子化出来的一般形式的函数为 \(G'(x)\)。现在考虑分母如何处理。
发现这个形式也是经典形式了,我们通过广义二项式定理展开后有
设上面这个东西是 \(G''(x)\),那么就有 \(G(x)=G'(x)\times G''(x)\)。
由于我们已经差分过了,因此我们现在要求的是形如
的东西。这等价于
然后根据二次项系数的递推式推一推就有
由于我们是暴力把 \(G'(x)\) 的系数都推出来了的,因此可以说我们已经做完了。但是还有一个问题:组合数如何在 \(\mod 2004\) 的意义下求出来?
一种特殊的计算方法是先计算组合数分子上的数在 \(\mod (2004\times n!)\) 意义下的值,再将答案除以 \(n!\) 再 \(\mod 2004\) 即可。可以通过这种东西绕开 exLCS。
然后就做完了。具体的暴力计算 \(G'(x)\) 的系数就可以写一个拆分即可一个一个算复杂度就是对的。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=15,p=2004;
int n,l,r,m[N],P,ni=1ll,ans=0;
int C(int x,int y){
int res=1ll;
for(int i=y;i>=y-x+1;i--)res=res*i%P;
return (res/ni)%p;
}
void dfs(int i,int sign,int k,int a){
if(k>a)return;
if(i==n+1){
ans=(ans+sign*C(n,n+a-k)%p+p)%p;
return;
}
dfs(i+1,sign,k,a),dfs(i+1,-sign,k+m[i]+1,a);
}
int solve(int a){
ans=0;dfs(1,1,0,a);return ans;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>l>>r;for(int i=1;i<=n;i++)cin>>m[i],ni*=i;
P=p*ni;
cout<<((solve(r)-solve(l-1)+p)%p);
return 0;
}
P4948 数列求和
发现形式很优美,包含了一个固定次数的 \(i^k\)。
考虑一下 \(\sum_i i^k\) 是怎么做的。考虑所谓扰动法。
扰动法一般是想去构造一个与原问题相关的方程然后将其转化为递推关系去做。
比如 \(\sum_i i^k\) 这个问题,一般就是直接设 \(F(k)=\sum_{i=1}^n i^k\),然后去考虑添加一些扰动项,将其转化一下:
这个东西也是一个套路,就是将扰动项合并进去弄出一个形如 \((i+1)^k\) 的形式然后就可以二项式定理展开
然后有一个同样的套路是将这两个不相干的和式交换以后有
发现最后刚好是 \(F\) 的形式,直接提出来
跟之前的放在一起
于是预处理一下组合数,复杂度 \(O(n^2)\) 的递推即可。所以为什么不用线性的拉格朗日插值呢?
说了这么多实际上还是没有进入主题。但是这两个东西实际上很类似,几乎只有形式上的不同。
同样的,加扰动:
经典二项式定理展开
经典交换和式
经典发现形式相同。设 \(F(k)=\sum_{i=1}^n a^{i}i^k\)。与上面的合起来
于是 \(F(k)\) 就是答案。实际实现是参考的 @mrsrz 的 思路,设的是 \(F(k)=\sum_{i=1}^{n-1}i^ka^i\),形式上是类似的,就不再赘述。复杂度 \(O(k^2)\)。
注意到 \(a=1\) 的时候要特判。发现就是上面说的东西。但是为了省事我直接写了个拉格朗日插值,也没有优化,反正 \(O(k^2)\) 的也能过。
code
写了个类似于记忆化搜索的东西,相对直观(?)
同时注意一开始的 \(n\) 就很大,在快速幂的时候需要先取了模再乘。因为这个调了半个小时。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=2005,p=1e9+7;
int pre[N],ni[N],n,a,F[N],vis[N],s[N];
int C(int x,int y){if(x>y)return 0;return pre[y]*ni[y-x]%p*ni[x]%p;}
int ksm(int x,int k){
int res=1;x%=p;//注意这个地方 x 可能与 n 同阶导致本身就很大!!!
while(k){
if(k&1)res=res*x%p;
x=x*x%p;k>>=1;
}
return res;
}
int inv(int x){return ksm(x,p-2);}
void init(){
pre[0]=1;for(int i=1;i<=2001;i++)pre[i]=pre[i-1]*i%p;
ni[2001]=inv(pre[2001]);for(int i=2000;i>=0;i--)ni[i]=ni[i+1]*(i+1)%p;
vis[0]=1;F[0]=a*(ksm(a,n-1)-1)%p*inv(a-1)%p;
}
int get(int k){
if(vis[k])return F[k];
int tmp=0;vis[k]=1;
for(int j=0;j<=k-1;j++)tmp=(tmp+C(j,k)*get(j)%p)%p;
return F[k]=(ksm(n,k)*ksm(a,n)%p-a-a*tmp%p+p)%p*inv(a-1)%p;
}
void solve0(int k){
for(int i=1;i<=k+2;i++)s[i]=(s[i-1]+ksm(i,k))%p;
int ans=0;
for(int i=1;i<=k+2;i++){
int s1=1,s2=1;
for(int j=1;j<=k+2;j++){
if(i==j)continue;
s1=(s1*(n-j+p)%p+p*p)%p,s2=(s2*(i-j+p)%p+p*p)%p;
}
ans=(ans+s[i]*s1%p*inv(s2)%p)%p;
}
cout<<(ans+p)%p;return;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int k;cin>>n>>a>>k;
if(a==1ll){solve0(k);return 0;}
init();
cout<<(get(k)+ksm(n,k)*ksm(a,n)%p)%p<<'\n';return 0;
}
/*
100 1 2
*/
指数生成函数(EGF)
定义
可以注意到相比于 OGF 而言多了一个 \(\frac{1}{i!}\)。
基本运算
考虑提出来一个 \(i!\) 的系数转化成同样的形式。
可以发现这样的形式是相当优美的。我们可以将其看作序列
的指数生成函数。也就是说相对于普通生成函数的卷积,指数生成函数的卷积多了一个组合数。我们可以利用这个组合数来做一些对于排列的计数之类的东西。

浙公网安备 33010602011771号