看了jcvb的WC2015交流课件。虽然没懂后面的复合逆部分,但生成函数感觉受益良多。
指数生成函数
集合中大小为 i 的对象的权值是 ( a_i ) ,该集合的生成函数是 ( sumlimits_{i>=0} frac{a_i}{i!} x^i )
一个重要式子: ( sumlimits_{i=0}^{infty} frac{A^i}{i!} = e^A ) 。其中 A 可以是一个多项式。
对于有标号对象的计数。可以“拼接”,即 “大小为 i 的集合的带标号方案” 与 “大小为 j 的集合的带标号方案” ,想要维持相对大小地拼在一起变成 “大小为 i+j 的集合的带标号方案”。
“拼接” 的方法就是直接把两个 EGF 乘在一起。
一般来说,要取出 “有 n 个元素” 的方案,就是把 ( x^n ) 系数拿出来,乘上 n! 。这样就表示了组合数(因为集合的相对顺序无关,所以是组合数)。
但在 “拼接” 的过程中,不用担心 n! 的部分,直接把只有 ( frac{1}{i!} ) 和 ( frac{1}{j!} ) 的部分乘起来,就是正确的。
比如课件中举的例子:
( 2^{inom{n}{2}} ) 是 n 个点的简单无向图的方案数。直接乘了一个 ( frac{1}{n!} ) ,只看系数的话,是莫名其妙的东西。但是把 ( c_n ) 也乘上 ( frac{1}{n!} ) 作为 EGF 的系数,两者就可以直接 “拼接” 。最后的答案就是 ( C(x) ) 的 ( x^n ) 项系数再乘一个 ( n! ) 。
多项式 ln 相关
一个重要式子: ( sumlimits_{i>=1} frac{x^i}{i} = -ln(1-x) )
更普遍的形式其实是: ( ln(1+x) = sumlimits_{i>=1}frac{(-1)^{i-1}}{i} x^i )
与之相关的应用,这里举出课件上涉及的两个:
1.置换计数
轮换组成置换,适合用 EGF 的角度来看。
最后那步就是 ( exp(-ln(1-x)) = exp(ln(frac{1}{1-x})) = frac{1}{1-x} = sumlimits_{i=0}^{infty}x^i = sumlimits_{i=0}^{infty}frac{i!}{i!}x^i )
2.背包计数(无标号集合计数)
课件给出了三个版本的问题,并给出了第一个版本的解答。第二个版本就是自己写的,可能有错误,欢迎指正。第三个版本不太会……
不同种类算多种方案。
写出生成函数:( prodlimits_{i=1}^{n} ( sumlimits_{j>=0}( x^{i*j} )^{a_i} )
(=prodlimits_{i=1}^{n} ( frac{1}{1-x^i} )^{a_i} )
(=exp( sumlimits_{i=1}^{n} -a_i * ln( 1-x^i ) ) )
(=exp( sumlimits_{i=1}^{n}a_i sumlimits_{j>=1}frac{x^{i*j}}{j} ) ) (这里用了那个式子)
(=exp( sumlimits_{j>=1} frac{1}{j} sumlimits_{i=1}^{n} a_i * x^{i*j} ) )
(=exp( sumlimits_{j>=1} frac{1}{j} A(x^j) ) )
对于每个 j , A(x) 只有 j 的倍数次项的系数需要关注。复杂度可以做到调和级数 nlogn 。
写出生成函数:( prodlimits_{i=1}^{n} ( 1+x^i )^{a_i} )
(=exp( sumlimits_{i=1}^{n} a_i * ln(1+x^i) ) )
(=exp( sumlimits_{i=1}^{n} a_i sumlimits_{j>=1} frac{(-1)^{j-1}}{j} x^{i*j} ) )
(=exp( sumlimits_{j>=1} frac{(-1)^{j-1}}{j} A(x^j) ) )
写出生成函数:( prodlimits_{i=1}^{n} sumlimits_{j=0}^{a_i} x^{i*j} )
(=exp( sumlimits_{i=1}^{n} ln( frac{1-x^{j*(a_i+1)}}{1-x^i} ) ) )
然后就不会了……
这种背包计数的题,遇见两道,把自己的博客链接粘过来:
https://www.cnblogs.com/Narh/p/10396644.html
https://www.cnblogs.com/Narh/p/10405656.html
当初做这两个题的时候,用的是另一个式子来推导的:( ( ln( f(x) ) )' = frac{f'(x)}{f(x)} )
写成那样,分子就用多项式的样子写求导,即把指数搬下来;分母就是正常的式子,和分子乘一下,整个就变成一个好看的多项式的样子了。
然后积分也是用多项式的样子写,把系数搬到指数上,推出来的结果就是一样的。
不过不管是这个式子,还是这回介绍的那个式子,都不太明白为什么能这样等于过去……
看了yml的2018集训队论文,学习了一下概率生成函数。不过没懂方差那部分。
概率生成函数
概率生成函数就是以 “离散变量 x = i 的概率” 为第 i 项系数的生成函数。
考虑论文里涉及的三道例题。
一. CTSC2006 歌唱王国
题目:https://www.luogu.org/problemnew/show/P4548
令 F(x) 表示序列在第 i 个位置结尾的概率。 G(x) 表示序列在第 i 个位置没有结尾的概率(的普通生成函数)。
列方程:
( F(x)+G(x) = 1+G(x)*x )
左边是第 i 个位置随便的方案。右边是第 i-1 个位置没有结尾,然后又填一个字符的方案。那么第 i 个位置就也是随便的。+1表示第 0 个位置,因为该位置没有 “第 i-1 个位置”。
( G(x)(frac{1}{m}x)^L = sumlimits_{i=1}^{L}a_i F(x) (frac{1}{m}x)^{L-i} )
其中, ( a_i ) 表示 [ A[ 1,i ] = A[ len-i+1 , len ] ] ,(A是特定序列)。
左边表示给第 i 个位置后面接上那个特定序列。这样它一定结束了。考察这 L 个接上的位置,发现在 L 的中间也可能已经结束。
假如接了 i 个字符就结束,那么自己刚才填的是特定序列的前 i 个字符,结束表示这 i 个字符也是特定序列的后 i 个字符。所以有右边的式子。
第一个方程,两边求导((a+b)' = a' + b'),得到 ( F'(x)+G'(x) = G'(x)*x + G(x) )
因为要求的是 F'(1) ,所以把两个式子的 x 都代入 1 ,得到
( F'(1) = G(1) )
( G(1) = sumlimits_{i=1}^{L} a_i F(1) m^i ) (把左边的 ( (frac{1}{m}x)^L ) 放到右边)
又有 F(1)=1 ,所以 ( F'(1) = sumlimits_{i=1}^{L} a_i m^i )
那个 ( F'(1) = G(1) ) ,有 “ 长度的期望 = ( sumlimits_{i=0}^{infty} ) 在 i 处没有结尾的概率 ” 这样的理解。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; int rdn() { int ret=0;bool fx=1;char ch=getchar(); while(ch>'9'||ch<'0'){if(ch=='-')fx=0;ch=getchar();} while(ch>='0'&&ch<='9')ret=ret*10+ch-'0',ch=getchar(); return fx?ret:-ret; } const int N=1e5+5,mod=1e4; int n,m,a[N],nxt[N],bin[N]; int main() { m=rdn(); n=1e5;bin[0]=1; for(int i=1;i<=n;i++) bin[i]=bin[i-1]*m%mod; int T=rdn(); while(T--) { n=rdn();for(int i=1;i<=n;i++)a[i]=rdn(); for(int i=2;i<=n;i++) { int cr=nxt[i-1]; while(cr&&a[cr+1]!=a[i])cr=nxt[cr]; if(a[cr+1]==a[i])nxt[i]=cr+1; else nxt[i]=0; } int cr=nxt[n], ans=bin[n]; while(cr) { ans=(ans+bin[cr])%mod; cr=nxt[cr]; } printf("%04d ",ans); } return 0; }
二. SDOI2017 硬币游戏
题目:https://www.luogu.org/problemnew/show/P3706
令 ( F_j(x) ) 表示 i 位置,用串 j 结尾的概率生成函数。 G(x) 表示 i 位置还没结尾的概率。
用理解列式子: ( sumlimits_{k=1}^{n} F_k(1) = 1 ) (用求导的那个方法推,也是一样)
又有,对于第 k 个串: ( G(x)(frac{1}{2}x)^{L_k} = sumlimits_{i=1}^{L_k} sumlimits_{j=1}^{n} a_{k,j,i} ( frac{1}{2}x )^{L_k -i} )
其中, ( a_{k,j,k} ) 表示 [ k 串的 i 长度前缀 = j 串的 i 长度后缀 ] 。
即 ( G(1) = sumlimits_{i=1}^{L_k} sumlimits_{j=1}^{n} a_{k,j,i} 2^i f_j(1) )
其实这样就可以高斯消元了!
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<cstdio> #include<cstring> #include<algorithm> #include<cmath> #define ll long long #define db double using namespace std; const int N=305,b1=10009,m1=1e9+9,b2=10007,m2=993244853; int upt1(int x){while(x>=m1)x-=m1;while(x<0)x+=m1;return x;} int upt2(int x){while(x>=m2)x-=m2;while(x<0)x+=m2;return x;} int n,m,bn1[N],bn2[N],h1[N][N],h2[N][N]; db bin[N],a[N][N],f[N]; char s[N]; pair<int,int> get_h(int x,int l) { int a=upt1(h1[x][m]-(ll)bn1[m-l+1]*h1[x][l-1]%m1); int b=upt2(h2[x][m]-(ll)bn2[m-l+1]*h2[x][l-1]%m2); return make_pair(a,b); } void gauss(int n) { for(int i=1;i<=n;i++) { int cr=i; for(int j=i+1;j<=n;j++) if(fabs(a[j][i])>fabs(a[cr][i]))cr=j; //if(!a[cr][i])break; for(int j=i;j<=n+1;j++)swap(a[i][j],a[cr][j]); for(int j=i+1;j<=n;j++) { db sl=a[j][i]/a[i][i]; for(int k=i;k<=n+1;k++) a[j][k]-=sl*a[i][k]; } } for(int i=n;i;i--) { f[i]=a[i][n+1]/a[i][i];//// /a[i][i] for(int j=i-1;j;j--) a[j][n+1]-=f[i]*a[j][i]; } } int main() { scanf("%d%d",&n,&m); bn1[0]=bn2[0]=1; for(int i=1;i<=m;i++)bn1[i]=(ll)bn1[i-1]*b1%m1; for(int i=1;i<=m;i++)bn2[i]=(ll)bn2[i-1]*b2%m2; for(int i=1;i<=n;i++) { scanf("%s",s+1); for(int j=1;j<=m;j++) { int w; if(s[j]=='H')w=1; else w=2; h1[i][j]=((ll)h1[i][j-1]*b1+w)%m1; h2[i][j]=((ll)h2[i][j-1]*b2+w)%m2; } } bin[0]=1;for(int i=1;i<=m;i++)bin[i]=bin[i-1]*2; for(int k=1;k<=n;k++) { for(int j=1;j<=n;j++) for(int i=1;i<=m;i++) if(make_pair(h1[k][i],h2[k][i])==get_h(j,m-i+1)) a[k][j]+=bin[i]; a[k][n+1]=-1;//G(1) } for(int i=1;i<=n;i++)a[n+1][i]=1; a[n+1][n+2]=1; gauss(n+1); for(int i=1;i<=n;i++)printf("%.10f ",f[i]); return 0; }
关于本题的另一种想法:https://www.cnblogs.com/Narh/p/10407245.html
三. Dice
题目:http://acm.hdu.edu.cn/showproblem.php?pid=4652
题意:m 面的骰子。求:1.期望多少次,可以使得 “最后 n 次投出来的结果相同” ;2.期望多少次,可以是的 “最后 n ( n<=m ) 次投出来的结果互不相同” 。
对于第一问,令 ( F_j(x) ) 表示以第 j 面重复 n 次结束的概率,( G(x) ) 表示不结束的概率。
所有面等价,所以 ( sum F_j(1) = 1 ==> F_j(1) = frac{1}{m} )
又有 ( sum F'_j(1) = G(1) ) , ( G(1) ) 就是答案。
( G(x)( frac{1}{m}x )^n = sumlimits_{i=1}^{n} F_k(x) ( frac{1}{m}x )^{L-i} ) ,其中 k 是某一面。
( ==> G(1) = sumlimits_{i=1}^{n} F_k(x) m^i = sumlimits_{i=1}^{n} frac{1}{m}*m^i = sumlimits_{i=0}^{n} m^i )
对于第二问,有 ( ( tot= inom{m}{n}*n! = frac{m!}{(m-n)!} ) ) 种结尾。令 ( F_j(x) ) 表示以第 j 种结尾结束的概率, ( G(x) ) 表示不结束的概率。
所有结尾等价,所以 ( sum F_j(1) = 1 ==> F_j(1) = frac{(m-n)!}{m!} )
又有 ( sum F'_j(1) = G(1) ) , ( G(1) ) 就是答案。
( G(x)(frac{1}{m}x)^n = sumlimits_{i=1}^{n} sumlimits_{j=1}^{tot} a_{k,j,i} F_j(x) (frac{1}{m}x)^{n-i} )
其中, ( a_{k,j,i} ) 表示 [ 第 k 个结尾的前 i 个字符 == 第 j 个结尾的后 i 个字符 ] 。
( G(1) = sumlimits_{i=1}^{n} frac{(m-n)!}{m!} * m^i sumlimits_{j=1}^{tot} a_{k,j,i} )
其实 ( sumlimits_{j=1}^{tot} a_{k,j,i} ) 是能算的。已知 i 个位置相等,剩下位置的方案就是 ( inom{m-i}{n-i} * (n-i)! = frac{(m-i)!}{(m-n)!} )
( G(1) = sumlimits_{i=1}^{n} frac{(m-i)!}{m!}*m^i = sumlimits_{i=1}^{n} frac{m^i}{m的i次下降幂} )
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<cstdio> #include<cstring> #include<algorithm> #define db double using namespace std; const int N=1e6+5; int op,n,m; int main() { int T; scanf("%d",&T); while(T--) { scanf("%d%d%d",&op,&m,&n); if(!op) { int ml=1,ans=0; for(int i=0;i<n;i++) { ans+=ml; if(i<n-1)ml*=m;} printf("%d ",ans); } else { db ans=0,ml=1; for(int i=1,j=m;i<=n;i++,j--) { ml=ml/j*m; ans+=ml;} printf("%.10f ",ans); } } return 0; }