【9】斯特林数学习笔记

前言

【6】组合计数学习笔记 当作一类具体的问题提到过,但是这个东西好像使用范围比较广,查了一下这个知识点在大纲中,单独开一篇博客记录一下。

统一了斯特林数的记号,但有一些高级的内容涉及生成函数,并没有记载。

第二类斯特林数

基础定义

\(n\) 个有区别的球放到 \(m\) 个相同的盒子中,要求无一空盒,其不同的方案数用 \(n\brace{m}\) 表示,称为第二类斯特林数。

设有 \(n\) 个不同的球,分别用 \(b_1,b_2,\dots b_n\) 表示。 从 \(1\) 到 \(n\) 依次考虑每个球的放法,从中取出一个球 \(b_n\),\(b_n\)的放法有以下两种。

\(1\) :\(b_n\) 独自占一个盒子。

那么剩下的球只能放在 \(m-1\) 个盒子中,方案数为 \({n-1}\brace{m-1}\)。

\(2\) :\(b_n\) 与别的球共占一个盒子。

那么可以事先将 \(b_1,b_2,\dots b_{n-1}\) 这 \(n-1\) 个球放入 \(m\) 个盒子中,然后再将球 \(b_n\) 可以放入其中一个盒子中,方案数为 \(m{n-1\brace m}\)。

根据加法原理,得出第二类斯特林数的递推式。

\[{n\brace{m}}={n-1\brace m-1}+m{n-1\brace m} \]

边界情况为 \({0\brace0}=1\)。

使用递推法计算第二类斯特林数的时间复杂度为 \(O(mn)\),但可以一次求出范围内所有第二类斯特林数的值。

通项公式

\[{n\brace m}=\sum_{i=0}^m\frac{(-1)^{m-i}i^n}{i!(m-i)!} \]


如果 \(n\) 个不同的球放进 \(m\) 个不相同的盒子,允许有空盒,那方案数为 \(m^n\)。

我们很容易把不相同的盒子转化相同的盒子,考虑编号的排列,除掉 \(m!\) 即可。

考虑容斥原理。枚举钦定空盒数量 \(k\),如果剩余随便放,则答案为 \((m-i)^n\)。根据容斥原理,如果 \(k\) 为偶数,就加上这种情况的贡献;如果 \(k\) 为奇数,就减去这种情况的贡献。又由于需要选定钦定哪 \(k\) 个空盒,还需乘上 \(\binom{m}{k}\),推出以下式子。

\[{n\brace m}=\frac{1}{m!}\sum_{k=0}^m(-1)^k\binom{m}{k}(m-k)^n \]

换元,用 \(i\) 代换 \(m-k\)。

\[{n\brace m}=\frac{1}{m!}\sum_{i=0}^m(-1)^{m-i}\binom{m}{m-i}i^n \]

用阶乘展开组合数,把 \(\frac{1}{m!}\) 乘进去,就得到了最后的式子。

\[{n\brace m}=\frac{1}{m!}\sum_{i=0}^m\frac{(-1)^{m-i}i^nm!}{i!(m-i)!}=\sum_{i=0}^m\frac{(-1)^{m-i}i^n}{i!(m-i)!} \]


使用通项公式计算第二类斯特林数的时间复杂度为 \(O(m\log n)\),需要快速幂和预处理阶乘逆。

斯特林反演

\[x^n=\sum_{i=0}^n{n\brace i}{\binom{x}{i}i!} \]


考虑组合意义。组合意义为有 \(n\) 个有区别的球装到 \(x\) 个有区别的盒子里。

左边式子就是考虑每一个球可以放到 \(x\) 个盒子中,有 \(n\) 中放法,根据乘法原理有 \(x^n\) 种放法。

右边式子就是考虑选择多少个盒子放球,枚举放球的盒子的数量 \(i\),首先有 \(\binom{x}{i}\) 种选盒子编号的法,然后假设这些盒子全部相同,\(n\) 个有区别球放进 \(i\) 个相同的盒子里,方案数为 \(n\brace i\)。因为盒子互不相同,最后再给盒子分配编号,有 \(i!\) 种选法。根据乘法原理和加法原理,就是右边的式子。


这个式子可以实现对高次项进行转化,将指数化为枚举,是一个降次的方式。同时,这个式子也与上升幂和下降幂以及其转化有关,这里暂不展开。

第一类斯特林数

如果对于一个排列,我们把这个排列的循环移位视为与此排列是同一种方案,我们把这种排列称为圆排列。可以想象圆排列是在圆桌上排座位。

长度为 \(n\) 的圆排列数量为 \(\frac{n!}{n}\),因为每一个不同的排列都被计算了 \(n\) 次。

\(n\) 个有区别的球放到 \(m\) 个相同的圆排列中,要求无一空圆排列,其不同的方案数用 \({n\brack m}\) 表示,称为第一类斯特林数。

设有 \(n\) 个不同的球,分别用 \(b_1,b_2,\dots b_n\) 表示。 从 \(1\) 到 \(n\) 依次考虑每个球的放法,从中取出一个球 \(b_n\),\(b_n\)的放法有以下两种。

\(1\) :\(b_n\) 独自占一个圆排列。

那么剩下的球只能放在 \(m-1\) 个圆排列中,方案数为 \({n-1\brack m-1}\)。

\(2\) :\(b_n\) 与别的球共占一个圆排列。

那么可以事先将 \(b_1,b_2,\dots b_{n-1}\) 这 \(n-1\) 个球放入 \(m\) 个圆排列中。因为圆排列可以看作一个环,我们发现每一个大小为 \(x\) 的圆排列中有 \(x\) 个插入的位置,总共就有 \(n-1\) 个插入的位置,方案数为 \((n-1){n-1\brack m}\)。

根据加法原理,得出第一类斯特林数的递推式。

\[{n\brack{m}}={n-1\brack m-1}+(n-1){n-1\brack m} \]

边界情况为 \({0\brack0}=1\)。

使用递推法计算第一类斯特林数的时间复杂度为 \(O(mn)\),但可以一次求出范围内所有第二类斯特林数的值。第一类斯特林数没有实用的通项公式。

例题

例题 \(1\) :

P1655 小朋友的球

第二类斯特林数模板题,需要高精度。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
int n,m;
int f[101][101][101]; 
void huge_int(int na,int nb,int a,int b,int c,int d,int m)
{
	int flag=0;
	for(int i=1;i<=100;i++)
	    f[na][nb][i]=f[a][b][i];
	for(int i=100;i>0;i--)
	    {
	    	f[na][nb][i]+=f[c][d][i]*m+flag;
	    	flag=f[na][nb][i]/10;
	    	f[na][nb][i]%=10;
		}
}

void print(int n,int m)
{
	int now=1;
	for(now=1;now<=100;now++)
	    if(f[n][m][now]!=0)break;
	for(int i=now;i<=100;i++)
	    printf("%d",f[n][m][i]);
}

int main()
{
	for(int i=1;i<=100;i++)f[i][1][100]=f[i][i][100]=1;
	for(int i=1;i<=100;i++)
	    for(int j=1;j<=100;j++)
	        if(!(i==j||j==1))huge_int(i,j,i-1,j-1,i-1,j,j);
	while(scanf("%d%d",&n,&m)!=-1)
	     {
	     if(n<m)
	        {
	        	printf("0\n");
	        	continue;
			}
	     print(n,m);
	     putchar('\n');
	     }
	return 0;
}

例题 \(2\) :

CF1342E Placing Rooks

首先答案满足每行有一个棋子或每列有一个棋子,否则假设某一行没有棋子,则为了覆盖这一行需要每一列都有一个棋子,矛盾。某一列没有棋子同理。因此,最多只能把棋子摆成一排,产生 \(k-1\) 对碰撞。

行和列等价,我们先考虑每行有一个棋子的情况。假设每一列都只有一颗棋子,如果需要产生 \(k\) 对棋子可以相互攻击,则需要将 \(k\) 列的棋子移动到别的列,最后总共有 \(n-k\) 列有棋子。

首先选出 \(n-k\) 列的编号,方案数为 \(\binom{n}{n-k}\)。然后假设每列相同,由于棋子所在的行不同所以本质不同,相当于有区别的求放进无区别的盒子里,方案数为 \(n\brace n-k\)。最后由于每列不同,分配编号,方案数为 \((n-k)!\)。最终的式子为 \(\binom{n}{n-k}{n\brace n-k}(n-k)!\)。第二类斯特林数可以直接使用通项公式计算。

注意 \(k\ne0\) 时还需要考虑每列有一个棋子的情况,与每行有一个棋子的情况相同,乘以 \(2\) 即可。时间复杂度 \(O(m\log n)\)。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
long long n,k,jc[300000],inv[300000],ans=0;
const long long mod=998244353;
long long power(long long a,long long p)
{
	long long x=a,ans=1;
	while(p)
	   {
	   	if(p&1)ans=ans*x%mod;
	   	p>>=1;
	   	x=x*x%mod;
	   }
	return ans;
}

long long c(long long n,long long k)
{
	return jc[n]*inv[n-k]%mod*inv[k]%mod;
}

long long strl(long long n,long long m)
{
	long long ans=0;
	for(int i=0;i<=m;i++)ans=(ans+power(-1,m-i)*power(i,n)%mod*inv[i]%mod*inv[m-i]%mod)%mod;
    return (ans%mod+mod)%mod;
}

int main()
{
	jc[0]=1;
	for(int i=1;i<=200000;i++)jc[i]=jc[i-1]*i%mod;
	inv[200000]=power(jc[200000],mod-2)%mod;
	for(int i=199999;i>=0;i--)inv[i]=inv[i+1]*(i+1)%mod;
	scanf("%lld%lld",&n,&k);
	if(k>=n)printf("0\n");
	else if(k==0)printf("%lld\n",c(n,n-k)%mod*strl(n,n-k)%mod*jc[n-k]%mod);
	else printf("%lld\n",2*c(n,n-k)%mod*strl(n,n-k)%mod*jc[n-k]%mod);
	return 0;
}

例题 \(3\) :

P4609 [FJOI2016] 建筑师

我们发现,最高的建筑无论从左边还是从右边都可以被看到,那我们先不管它。

考虑左边的建筑的形式,发现一定是 \(A-1\) 组建筑,每组建筑最左边的建筑比这个组左边的组的建筑都高,而这组内其他建筑都比最左边的建筑矮。右边的建筑的形式也是同理的 \(B-1\) 组建筑,只不过最高的在最右边。

我们考虑组内怎么分配。显然,\(n\) 个元素组内除了最高的元素必须在最左边或者最右边,其余元素的排布没有限制,因此方案数为 \((n-1)!=\frac{n!}{n}\)。观察式子,发现其实就是圆排列。

接下来考虑把除了最高的元素分配到这 \(A+B-2\) 个组中。我们发现,无论怎么分配,只要不存在空的组,一定可以按照组内最大的元素把每个组排好序,且两种方案必不相同。因此,相当于 \(n-1\) 个有区别的球放进 \(A+B-2\) 个圆排列中,方案数为 \(n-1\brack A+B-2\)。

最后还需要选择 \(A-1\) 个组丢到左边去,所以还需要乘上 \(\binom{A+B-2}{A-1}\)。由于 \(A,B\) 很小,预处理之后可以做到 \(O(1)\) 查询。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
int t,n,a,b,strl[50001][201],c[201][201];
const int mod=1e9+7;
int main()
{
	strl[0][0]=1;
	for(int i=1;i<=50000;i++)
	    for(int j=1;j<=200;j++)
	        strl[i][j]=(strl[i-1][j-1]+1ll*strl[i-1][j]*(i-1)%mod)%mod;
	for(int i=0;i<=200;i++)c[i][0]=c[i][i]=1;
	for(int i=1;i<=200;i++)
	    for(int j=1;j<i;j++)
	        c[i][j]=(c[i-1][j-1]+c[i-1][j])%mod;
	scanf("%d",&t);
	while(t--)
		{
		scanf("%d%d%d",&n,&a,&b);
		printf("%lld\n",1ll*strl[n-1][a+b-2]*c[a+b-2][a-1]%mod);
	    }
	return 0;
}

例题 \(4\) :

P6620 [省选联考 2020 A 卷] 组合数问题

多项式显然是在诈骗,考虑展开后求每一项,最后每一项乘上系数累加。先写出每一项的式子。

\[\sum_{k=0}^{n}k^mx^k\binom{n}{k} \]

\(n\) 非常大,而 \(m\) 比较小。而 \(m\) 是指数,考虑经典的斯特林反演技巧,带入斯特林反演公式把 \(m\) 变成枚举量。

\[=\sum_{k=0}^{n}x^k\binom{n}{k}\sum_{i=0}^m{m\brace i}\binom{k}{i}i! \]

交换枚举顺序,把对 \(m\) 的枚举提到前面去。

\[=\sum_{i=0}^m{m\brace i}i!\sum_{k=0}^{n}x^k\binom{n}{k}\binom{k}{i} \]

对 \(n\) 求和后面有两项组合数相乘有点麻烦,考虑把一个提出去。根据组合意义,\(\binom{n}{k}\binom{k}{i}\) 表示先在 \(n\) 个中选 \(k\) 个,再在 \(k\) 中选 \(i\) 个,等价的做法是先在 \(n\) 个中选 \(i\) 个,再在 \(n-i\) 中选 \(k-i\) 个,即 \(\binom{n}{i}\binom{n-i}{k-i}\)。这样 \(\binom{n}{i}\) 就可以提出去了。

\[=\sum_{i=0}^m{m\brace i}i!\binom{n}{i}\sum_{k=0}^{n}x^k\binom{n-i}{k-i} \]

考虑把对 \(n\) 求和后面配成二项式定理的形式消去求和。由于单组合数不好变形,考虑对指数进行变形。二项式定理要求指数和组合数下指标相同,所以考虑提一个 \(x^i\) 来配凑。由于 \(k\lt i\) 时 \(\binom{n-i}{k-i}=0\),所以可以直接忽略 \(k\lt i\) 的项。

\[=\sum_{i=0}^m{m\brace i}i!\binom{n}{i}x^i\sum_{k=i}^{n}x^{k-i}\binom{n-i}{k-i} \]

换元,用 \(p\) 代 \(k-i\)。注意换元后变量的范围。

\[=\sum_{i=0}^m{m\brace i}i!\binom{n}{i}x^i\sum_{p=0}^{n-i}x^{p}\binom{n-i}{p} \]

配凑一个 \(1^{n-i-p}\) 项,就可以愉快地二项式定理了。

\[=\sum_{i=0}^m{m\brace i}i!\binom{n}{i}x^i(x+1)^{n-i} \]

由于 \(n\) 很大,目前 \(\binom{n}{i}\) 求不出来。考虑 \(i!\binom{n}{i}=\frac{n!}{(n-i)!}=\prod_{j=n-i+1}^nj\),考虑维护变化,在枚举 \(i\) 的这一轮计算完成之后乘上一个 \(n-i\),就维护好了这两项。递推求第二类斯特林数加上快速幂就做完了。时间复杂度 \(O(m^2\log m)\)。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
long long n,x,mod,k,m,a,s2[2000][2000],ans=0;
long long power(long long a,long long p)
{
	long long x=a,ans=1;
	while(p)
	   {
	   	if(p&1)ans=ans*x%mod;
	   	p>>=1;
	    x=x*x%mod;
	   }
	return ans;
}

int main()
{
	scanf("%lld%lld%lld%lld",&n,&x,&mod,&k);
	s2[0][0]=1;
	for(int i=1;i<=1000;i++)
	    for(int j=1;j<=1000;j++)
	        s2[i][j]=(s2[i-1][j]*j%mod+s2[i-1][j-1])%mod;
	for(int m=0;m<=k;m++)
	    {
	    	long long sum=0,ac=1;
	    	scanf("%lld",&a);
	    	for(int i=0;i<=m;i++)sum=(sum+s2[m][i]*ac%mod*power(x,i)%mod*power(x+1,n-i)%mod)%mod,ac=ac*(n-i)%mod;
			ans=(ans+sum*a%mod)%mod;
		}
	printf("%lld\n",ans);
	return 0;
}

后记

涉及到上升幂和下降幂的内容 可以直接转化为斯特林反演的式子给出的形式,除非题目里直接出现上升幂和下降幂,否则应该不会用到。

世味年来薄似纱,谁令骑马客京华。

小楼一夜听春雨,深巷明朝卖杏花。

矮纸斜行闲作草,晴窗细乳戏分茶。

素衣莫起风尘叹,犹及清明可到家。

相关推荐
墨风如雪2 天前
8B 模型吊打 671B?数学证明界“卷王”Goedel-Prover-V2 来了!
数学·aigc
minglie14 天前
高斯代数基本定理的一种证明
数学
minglie14 天前
代数基本定理最简短的证明
数学
极大理想4 天前
可数集与不可数集
数学·几何学·拓扑学
盼满天繁星4 天前
关于模考 T2
数学
让我们一起加油好吗5 天前
【基础算法】倍增
数学·算法·快速幂·洛谷·倍增
金色光环7 天前
概率论:理解区间估计【超详细笔记】
笔记·数学·概率论·数理统计·区间估计
w90958 天前
【7】卡特兰数学习笔记
数学·学习笔记
MPCTHU8 天前
一个猜想不等式的推广
数学