斯特林数学习笔记
第一类斯特林数
定义
将 \(n\) 个互不相同的元素,划分为 \(k\) 个互不区分的非空轮换的方案数,记为 \(s(n,k)\),或 $n \brack m $。
一个轮换就是一个首尾相接的环形排列。如轮换 \([A,B,C,D]\),我们认为 \([A,B,C,D]=[B,C,D,A]=[C,D,A,B]=[D,A,B,C]\)。即,两个可以通过旋转而相互得到的轮换是等价的。需要注意,通过翻转得到的轮换不等价,如:\([A,B,C,D] \ne [D,C,B,A]\)。
第一类斯特林数也可以形象理解为:\(n\) 个人坐 \(m\) 张圆桌(没有空桌)的方案数。
递推式
\({n \brack k} = {n-1 \brack k-1}+(n-1){n-1 \brack k}\)。
其中边界为 \({n \brack 0}=[n==0]\)。
递推式的证明可以考虑组合意义,新加入一个元素时,有两种方案:
-
将该元素单独至于一个轮换中,那么共有 \(n-1 \brack k-1\) 种方案。
-
将该元素加入当任意一个现有的轮换中,共有 \((n-1) {n-1 \brack k}\) 种方案。
第二类斯特林数
将 \(n\) 个互不相同的元素,划分为 \(m\) 个互不区分的非空子集的方案数。记为 \(n \brace m\),也可记为 \(S(n,m)\)
递推式
\({n \brace k}={n-1 \brace k-1}+k {n-1 \brace k}\)
其中边界为 \({n \brace 0}=[n==0]\)。
递推式的证明可以考虑组合意义,新加入一个元素时,有两种方案:
-
将该元素单独至于一个新集合中,那么共有 \(n-1 \brace k-1\) 种方案。
-
将该元素加入当任意一个现有的轮换中,共有 \(k {n-1 \brace k}\) 种方案。
通项公式
利用二项式反演,可以得到第二类斯特林数的通项公式:
\({n \brace m}=\dfrac{\sum_{i=0}^{m} (-1)^{m-i} \binom{m}{i} i^n =\sum_{i=0}^{m}}{m!} =\sum_{i=0}^{m} \dfrac{(-1)^{m-i} i^n}{i!(m-i)!}\)
建筑师
对于一个 \(1,2,\ldots,n\) 的排列,设有 \(A\) 个数的左边都比它小,\(B\) 个数的右边都比它小,求满足的排列个数。
\(T\) 组询问,答案对 \(998244353\) 取模。
\(1 \leq n \leq 50000, \ 1 \leq A, B \leq 100, \ 1 \leq T \leq 200000\)。
思路:
不难发现,\(n\) 一定满足题意,考虑以 \(n\) 为界限,在其左侧的属于 \(A\),在其右侧的属于 \(B\)。
设第 \(i\) 个位置上的数为 \(p_i\),\(pre_i=\max_{1\leq j \leq i} p_i\)。
那么对于一个在 \(A\) 中的数字,一定满足 \(pre_i=p_i\),在 \(B\) 中的同理。
设 \(i,j\) 为前后两个在 \(A\) 中的数,那么对于 \(k \in (i,j)\),一定满足 \(p_k < pre_k\)。考虑将 \([i,j-1]\) 划分为一个区间,那么最终可以得到 \(A+B-1\) 个区间,如下图所示:
其中黑色的方块表示该点在 \(A\) 或 \(B\) 中,其中 \(n\) 单独成一个区间。
不难发现,划分为 \(A+B-1\) 个区间以后,区间内最大的数一定在区间的最左侧或最右侧。而对于其他的数,则可以全排列。
那么一个区间就等价于一个固定了开头的轮换,于是就可以套用第一类斯特林数,总的划分方案数为 \(n-1 \brack A+B-2\)。
而一个区间既可以放在 \(A\) 中,也可以放在 \(B\) 中,将区间分组的方案数就为 \(\binom{A+B-2}{A-1}\)。
于是对于每一组询问,答案就是 \(\binom{A+B-2}{A-1} {n-1 \brack A+B-2}\)。
code:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=50010,M=210,mod=1e9+7;
int n,A,B,c[N][M],s[N][M];
int main()
{
s[0][0]=1;for(int i=1;i<N;i++) for(int j=1;j<M;j++) s[i][j]=(s[i-1][j-1]+1ll*(i-1)*s[i-1][j]%mod)%mod;
for(int i=0;i<M;i++) for(int j=0;j<=i;j++) if(!j) c[i][j]=1;else c[i][j]=(c[i-1][j-1]+c[i-1][j])%mod;
int T;scanf("%d",&T);while(T--) scanf("%d%d%d",&n,&A,&B),printf("%d\n",1ll*c[A+B-2][A-1]*s[n-1][A+B-2]%mod);
return 0;
}
组合数问题
计算
的值。其中 \(n\), \(x\), \(p\) 为给定的整数,\(f(k)\) 为给定的一个 \(m\) 次多项式 \(f(k) = a_0 + a_1k + a_2k^2 + \cdots + a_mk^m\)。\(\binom{n}{k}\) 为组合数,其值为 \(\binom{n}{k} = \frac{n!}{k!(n-k)!}\)。
\(1\le n, x, p \le 10^9, 0\le a_i\le 10^9, 0\le m \le \min(n,1000)\)。
思路:
令 $x^{\underline{k} }=\prod_{i=x-k+1}^{x}=x(x-1)(x-2)\ldots (x-k+1) $。称为 \(x\) 的 \(k\) 次下降幂。
而对于单项式下降幂,与组合数存在以下联系:
考虑将多项式 \(f(k)=\sum_{i=0}^{m} a_i k^i\) 转换为 \(f(k)=\sum_{i=0}^{m} b_i k^{\underline{i} }\) 的形式。
对于一个单项式 \(x_n\),与第二类斯特林数存在以下等式:
形象地理解一下,\(x^n\) 可以看成是将 \(n\) 个小球分别放到 \(x\) 个盒子当中,允许有空盒子的方案数。那么就可以考虑枚举哪些盒子不为空,然后就可以套用第二类斯特林数。由于盒子是不同的,所以最后还要乘上一个阶层。
于是就可以将 \(f(k)\) 变形:
得到 \(b_i\) 的表达式:
于是 \(b_i\) 就可以 \(O(m^2)\) 预处理出来。
接下来考虑对原式进行变形:
利用上面的组合公式,可以得到:
注意到当 \(k<i\) 时,后面的组合数没有意义,考虑内层枚举 \(k-i\) ,得到:
此时不难发现,内层循环就是一个二项式的展开形式,利用二项式定理,可以得到:
由于题目中满足 \(m\leq 2000\),此时就可以直接枚举得到答案。
code:
#include<cstdio>
#include<cstring>
#include<algorithm>
const int N=2023;
int n,x,ans,p,m,a[N],b[N],s[N][N];
void Add(int &a,int b){a+=b;if(a>=p) a-=p;}
void Mul(int &a,int b){a=1ll*a*b%p;}
int mul(int a,int b){int res=1;while(b) ((b&1)&&(res=1ll*res*a%p,0)),a=1ll*a*a%p,b>>=1;return res;}
int main()
{
scanf("%d%d%d%d",&n,&x,&p,&m);for(int i=0;i<=m;i++) scanf("%d",&a[i]);s[0][0]=1;
for(int i=1;i<=m;i++) for(int j=1;j<=i;j++) s[i][j]=(s[i-1][j-1]+1ll*j*s[i-1][j]%p)%p;
for(int i=0;i<=m;i++) for(int j=i;j<=m;j++) Add(b[i],1ll*a[j]*s[j][i]%p);int n_i=1,x_i=1;
for(int i=0;i<=m;i++) Add(ans,1ll*b[i]*n_i%p*x_i%p*mul(x+1,n-i)%p),Mul(n_i,n-i),Mul(x_i,x);printf("%d\n",ans);
return 0;
}