Leetcode 斯特林数

本文详细介绍了斯特林数在组合数学中的两类应用,包括第一类斯特林数和第二类斯特林数。第一类斯特林数涉及将元素组成环的方案数,而第二类斯特林数则关注元素划分到非空集合的方案数。文章通过递推公式、数学归纳法和特殊性质展示了斯特林数的计算方法,并给出了实际问题的应用,如解锁仓库问题。此外,还探讨了斯特林数在算法实现中的应用,以及在求解逆序对问题中的动态规划策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

斯特林数

组合数学 —— 斯特林数

斯特林数主要处理的是把 n 个不同的元素分成 k 个集合或环的个数问题。分为第一类斯特林数和第二类斯特林数,其中第一类斯特林数还分成有符号和无符号两种。

第一类斯特林数

仅讨论有符号的第一类斯特林数。
第一类斯特林数表示的是将 n 个不同元素分成 k 个不同的环的方案数。两个环不相同当且仅当这两个环不能通过旋转得到。表示方法为: [ k n ] \left[^{n}_{k}\right] [kn]
考虑递推,把 n 个不同元素分成 k 个不同的环有两种转移。第一种,有可能是 n − 1 个不同元素分成 k − 1 个不同的环,当前的第 n 个独立成一个元素。第二种可能是 n − 1 个不同元素已经分好了 k 个不同的环,当前这个可以加进去。加在每个已有元素的逆时针方向(或顺时针方向,方向没有关系,只要统一即可)就不会出现重复,共有 n − 1 种方法,所以:

[ 0 0 ] = 1 \left[^{0}_{0}\right]=1 [00]=1

[ k n ] = [ k − 1 n − 1 ] + [ k n − 1 ] ∗ ( n − 1 ) \left[^{n}_{k}\right]=\left[^{n-1}_{k-1}\right]+\left[^{n-1}_{k}\right]*(n-1) [kn]=[k1n1]+[kn1](n1)

这就是第一类斯特林数的递推式,也可以写成:

s ( n , k ) = s ( n − 1 , k − 1 ) + s ( n − 1 , k ) ∗ ( n − 1 ) s(n,k)=s(n−1,k−1)+s(n−1,k)∗(n−1) s(n,k)=s(n1,k1)+s(n1,k)(n1)

性质

所以组合数学中很多定理的证明是通过构造一个问题,用两种不同的方法计算,由于是同一个问题,那么算出来的答案一定是相等的,从而证明等式,简称“算两次”。

第一类斯特林数有如下特殊性质:

1、 s ( 0 , 0 ) = 1 s ( 0 , 0 ) = 1 s(0,0)=1s(0,0)=1 s(0,0)=1s(0,0)=1
2、 s ( n , 0 ) = 0 ( n > 0 ) s ( n , 0 ) = 0 ( n > 0 ) s(n,0)=0 (n>0)s(n,0)=0 (n>0) s(n,0)=0(n>0)s(n,0)=0(n>0)
3、 s ( n , 1 ) = ( n − 1 ) ! s ( n , 1 ) = ( n − 1 ) ! s(n,1)=(n−1)!s(n,1)=(n−1)! s(n,1)=(n1)!s(n,1)=(n1)!
4、 s ( n , n − 1 ) = C 2 n s ( n , n − 1 ) = C n 2 s(n,n−1)=C2ns(n,n−1)=Cn2 s(n,n1)=C2ns(n,n1)=Cn2
5、 s ( n , 2 ) = ( n − 1 ) ! ∗ ∑ n − 1 i = 11 i s ( n , 2 ) = ( n − 1 ) ! ∗ ∑ i = 1 n − 11 i s(n,2)=(n−1)!∗∑n−1i=11is(n,2)=(n−1)!∗∑i=1n−11i s(n,2)=(n1)!n1i=11is(n,2)=(n1)!i=1n11i
6、 s ( n , n − 2 ) = 2 C 3 n + 3 C 4 n s ( n , n − 2 ) = 2 C n 3 + 3 C n 4 s(n,n−2)=2C3n+3C4ns(n,n−2)=2Cn3+3Cn4 s(n,n2)=2C3n+3C4ns(n,n2)=2Cn3+3Cn4
7、 ∑ n k = 0 s ( n , k ) = n ! ∑ k = 0 n s ( n , k ) = n ! ∑nk=0s(n,k)=n!∑k=0ns(n,k)=n! nk=0s(n,k)=n!k=0ns(n,k)=n!

下面给出性质1,3,4,5的证明。

性质一
把 n 个元素排列起来,有 n! 种可能,首尾相接即可得到一个环。这里面每种情况重复了 n 次,因为可以旋转 n 次,所以除以 n,得到 s ( n , 1 ) = ( n − 1 ) ! s(n,1)=(n−1)! s(n,1)=(n1)!

性质三
s ( n , 2 ) = ( n − 1 ) ! ∗ ∑ i = 1 n − 11 i s ( n , 2 ) = ( n − 1 ) ! ∗ ∑ i = 1 n − 11 i s(n,2)=(n−1)!∗∑_i=1n−11is(n,2)=(n−1)!∗∑_i=1n−11i s(n,2)=(n1)!i=1n11is(n,2)=(n1)!i=1n11i
这里给出数学归纳法的证明。
首先, s ( 2 , 2 ) = 1 ∗ 1 = 1 s(2,2)=1∗1=1 s(2,2)=11=1,符合定义。
下面通过递推式和性质一证明性质三对于 n 也成立:

s ( n + 1 , 2 ) = s ( n , 1 ) + s ( n , 2 ) ∗ n = ( n − 1 ) ! + n ∗ ( n − 1 ) ! ∗ ∑ i = 1 n − 11 i = ( n − 1 ) ! + n ! ∗ ∑ i = 1 n − 11 i = n ! n + n ! ∗ ∑ i = 1 n − 11 i = n ! ∗ ∑ i = 1 n 1 i s ( n + 1 , 2 ) = s ( n , 1 ) + s ( n , 2 ) ∗ n = ( n − 1 ) ! + n ∗ ( n − 1 ) ! ∗ ∑ i = 1 n − 11 i = ( n − 1 ) ! + n ! ∗ ∑ i = 1 n − 11 i = n ! n + n ! ∗ ∑ i = 1 n − 11 i = n ! ∗ ∑ i = 1 n 1 i s(n+1,2)=s(n,1)+s(n,2)∗n=(n−1)!+n∗(n−1)!∗∑i=1n−11i=(n−1)!+n!∗∑i=1n−11i=n!n+n!∗∑i=1n−11i=n!∗∑i=1n1i s(n+1,2)=s(n,1)+s(n,2)∗n=(n−1)!+n∗(n−1)!∗∑i=1n−11i=(n−1)!+n!∗∑i=1n−11i=n!n+n!∗∑i=1n−11i=n!∗∑i=1n1i s(n+1,2)=s(n,1)+s(n,2)n=(n1)!+n(n1)!i=1n11i=(n1)!+n!i=1n11i=n!n+n!i=1n11i=n!i=1n1is(n+1,2)=s(n,1)+s(n,2)n=(n1)!+n(n1)!i=1n11i=(n1)!+n!i=1n11i=n!n+n!i=1n11i=n!i=1n1i
得证!

性质四
同理可用数学归纳法强行计算。巧妙的方法我还没想到。

性质五
这里有一个巧妙地“算两次”方法。
首先构造一个问题,求 n 个数的所有排列。
首先用乘法原理直接得出结论, a n s = n ! ans=n! ans=n!
我们知道,对于一个排列对应一个置换,把这个置换中的上下对应位置连边,可以得到许多的环。由于排列和置换是一一对应的,所以我们要求排列的个数,就是求用nn个元素组成环的方案数,所以我们枚举环的个数:

n ! = ∑ k = 1 n s ( n , k ) n!=∑k=1ns(n,k) n!=k=1ns(n,k)

由于我们有 s ( n , 0 ) = 0 s(n,0)=0 s(n,0)=0,所以也可以写成:

∑ k = 0 n s ( n , k ) = n ! ∑k=0ns(n,k)=n! k=0ns(n,k)=n!

第二类斯特林数
第二类斯特林数表示把nn个元素分成kk个非空集合的方案数,集合内是无序的。这样,我们很容易得出转移:
分为两种情况。第一种情况,如果前n−1n−1个元素组成了k−1k−1个非空集合,那么当前元素自成一个集合。第二种情况,如果前n−1n−1个元素组成了kk个集合,那么当前的这个元素可以放进这kk个集合中任意的一个。所以我们的到来递推方程:

[ k n ] = [ k − 1 n − 1 ] + [ k n − 1 ] ∗ k \left[^{n}_{k}\right]=\left[^{n-1}_{k-1}\right]+\left[^{n-1}_{k}\right]*k [kn]=[k1n1]+[kn1]k

也可以写成:

S ( n , k ) = S ( n − 1 , k − 1 ) + S ( n − 1 , k ) ∗ k S(n,k)=S(n−1,k−1)+S(n−1,k)∗k S(n,k)=S(n1,k1)+S(n1,k)k

性质
1、 S ( 0 , 0 ) = 1 S(0,0)=1 S(0,0)=1
2、 S ( n , 0 ) = 0 , n > 0 S(n,0)=0,n>0 S(n,0)=0,n>0
3、 S ( n , n ) = 1 S(n,n)=1 S(n,n)=1
4、 S ( n , 2 ) = S ( n − 1 , 1 ) + S ( n − 1 , 2 ) ∗ 2 = 1 + S ( n − 1 , 2 ) ∗ 2 = 2 n − 1 − 1 S(n,2)=S(n−1,1)+S(n−1,2)∗2=1+S(n−1,2)∗2=2n−1−1 S(n,2)=S(n1,1)+S(n1,2)2=1+S(n1,2)2=2n11
5、 S ( n , n − 1 ) = C 2 n S(n,n−1)=C2n S(n,n1)=C2n
6、 S ( n , n − 2 ) = C 3 n + 3 C 4 n S(n,n−2)=C3n+3C4n S(n,n2)=C3n+3C4n

简单巧妙的证明:我们分成两种情况,把 n 个不同的元素分成 n − 2 个集合有两种情况,分别是有一个集合有三个元素和有两个集合有两个元素。对于前者,我们选出三个元素为一个集合,其他的各成一个集合,这种情况的方案数就是 C 3 n C3n C3n。对于后者,先选出四个元素来,考虑怎么分配。当其中一个元素选定另一个元素形成同一个集合时,这种情况就确定了,所以是 3 C 4 n 3C4n 3C4n。加法原理计算和即得证。
7、 S ( n , 3 ) = 12 ( 3 n − 1 + 1 ) − 2 n − 1 S(n,3)=12(3n−1+1)−2n−1 S(n,3)=12(3n1+1)2n1 数学归纳法
8、 S ( n , n − 3 ) = C 4 n + 10 C 5 n + 15 C 6 n S(n,n−3)=C4n+10C5n+15C6n S(n,n3)=C4n+10C5n+15C6n 同性质六

通项公式:

S ( n , m ) = 1 m ! ∑ k = 0 m ( − 1 ) k C k m ( m − k ) n S(n,m)=1m!∑k=0m(−1)kCkm(m−k)n S(n,m)=1m!k=0m(1)kCkm(mk)n

大概就是容斥原理,k 枚举有多少个集合是空的,每种情况有 C m k C^k_m Cmk 种空集情况,n 个元素可以放进非空的 m − k 个集合中。这样求出来的答案是有序的,所以我们除以 m! 使得其变为无序。

此公式更直接的推导,可以使用生成函数。

定理

第一类斯特林数 S1(n, m) 表示的是将 n 个不同元素构成 m 个圆排列的数目。

递推式

设人被标上1, 2, … p,则将这 p 个人排成 m 个圆有两种情况:

在一个圆圈里只有标号为 p 的人自己,排法有 S1(n - 1, m - 1) 个。
p 至少和另一个人在一个圆圈里。
这些排法通过把 1, 2 … n - 1 排成 m 个圆再把 n 放在 1, 2 … n - 1 任何一人左边得到,因此第二种类型排法共有 (n - 1) * S1(n - 1, m) 种。

就是把 {1, 2, …, p} 划分到 k 个非空且不可区分的盒子,然后将每个盒子中的元素排成一个循环排列。

第一类 Stirling 数定理:

边界条件:

有 n 个人和 n 个圆,每个圆只有一个人;
如果至少有 1 个人,那么任何的安排都至少包含一个圆

应用举例

第一类斯特林数除了可以表示升阶函数和降阶函数的系数之外还可以应用到一些实际问题上,比如很经典的解锁仓库问题。

有 n 个仓库,每个仓库有两把钥匙,共 2n 把钥匙。同时又有 n 位官员。

①如何放置钥匙使得所有官员都能够打开所有仓库?(只考虑钥匙怎么放到仓库中,而不考虑官员拿哪把钥匙。)
②如果官员分成 m 个不同的部,部中的官员数量和管理的仓库数量一致。那么有多少方案使得,同部的所有官员可以打开所有本部管理的仓库,而无法打开其他部管理的仓库?(同样只考虑钥匙的放置。)

分析:

① 打开仓库将钥匙放入仓库构成一个环:1 号仓库放 2 号钥匙,2 号仓库放 3 号钥匙……n 号仓库放 1 号钥匙,这种情况相当于钥匙和仓库编号构成一个圆排列方案数是 (n - 1)! 种。

② 对应的将 n 个元素分成 m 个圆排列,方案数就是第一类斯特林数 S1(n, m),若要考虑官员的情况,只需再乘上 n! 即可。

算法实现

const int mod=1e9+7;//取模
LL s[N][N];//存放要求的第一类Stirling数
void init(){
    memset(s,0,sizeof(s));
    s[1][1]=1;
    for(int i=2;i<=N-1;i++){
        for(int j=1;j<=i;j++){
            s[i][j]=s[i-1][j-1]+(i-1)*s[i-1][j];
            if(s[i][j]>=mod)
                s[i][j]%=mod;
        }
    }
}

第二类斯特林数

定理

第二类斯特林数 S2(n, m) 表示的是把 n 个不同元素划分到 m 个集合的方案数。

递推式

元素在哪些集合并不重要,唯一重要的是各集合里装的是什么,而不管哪个集合装了什么。

考虑将前 n 个正整数,{1,2,…,n} 的集合作为要被划分的集合,把 {1,2,…,n} 分到 m 个非空且不可区分的集合的划分有两种情况:

那些使得 n 自己单独在一个集合的划分,存在有 S2(n-1,m-1) 种划分个数
那些使得 n 不单独自己在一个盒子的划分,存在有 mS2(n-1,m) 种划分个数
考虑第二种情况,n 不单独自己在一个盒子,也就是 n 和其他元素在一个集合里面,也就是说在没有放 n 之前,有 n-1 个元素已经分到了m 个非空且不可区分的盒子里面(划分个数为 S2(n-1,m)),那么现在问题是把 n 放在哪个盒子里面,此时有 m 种选择,所以存在有 m
S2(n-1,m)

综上,可得出第二类斯特林数定理:

边界条件:

应用举例

第二类斯特林数主要是用于解决组合数学中的放球模型,主要是针对于球之前有区别的放球模型:

1)n 个不同的球,放入 m 个无区别的盒子,不允许盒子为空。

方案数:S2(n,m),与第二类斯特林数的定义一致。

2)n 个不同的球,放入 m 个有区别的盒子,不允许盒子为空。

方案数:m!*S2(n,m),因盒子有区别,乘上盒子的排列即可。

3)n 个不同的球,放入 m 个无区别的盒子,允许盒子为空。

方案数:,枚举非空盒的数目即可。

4)n 个不同的球,放入 m 个有区别的盒子,允许盒子为空。

① 方案数: ,枚举非空盒的数目,注意到盒子有区别,乘上一个排列系数即可。
② 既然允许盒子为空,且盒子间有区别,那么对于每个球有 m 种选择,每个球相互独立。有方案数:

算法实现

const int mod=1e9+7;//取模
LL s[N][N];//存放要求的Stirling数
void init(){
    memset(s,0,sizeof(s));
    s[1][1]=1;
    for(int i=2;i<=N-1;i++){
        for(int j=1;j<=i;j++){
            s[i][j]=s[i-1][j-1]+j*s[i-1][j];
            if(s[i][j]>=mod)
                s[i][j]%=mod;
        }
    }
}

扩展:S(n, m) 的奇偶性

求 S(n,m) 的奇偶性实质就是求 S(n, m) % 2
对于第二类斯特林数,首先有:S[i][j]=S[i−1][j−1]+j∗S[i−1][j]

那么在模 2 的情况下 ,有:

当 j 为偶数:S[i][j] ≡(S[i−1][j−1]%2)
当 j 为奇数:S[i][j] ≡((S[i−1][j−1]+S[i−1][j])%2)
于是,可以倒过来,即有:

当 j 为偶数时:S[i][j] 会被加到 S[i+1][j+1] 和 S[i+1][j]
当 j 为奇数时:S[i][j] 会被加到 S[i+1][j+1]
于是,令 a=n−m,b=(m+1)/2,则答案就相当于把 a 个相同的球分成 b 组,每组个数可以为 0 的方案总数,即:C(b−1,a+b−1)

然后利用 C(n,m) 为奇数时有 n&m=n 的结论,进行判断即可

int calc(int n,int m){
    return (m&n)==n;
}
int main(){
    int n,m;
    scanf("%d%d",&n,&m);
    if(n==0&&m==0)//特判
        printf("1\n");
    else if(n==0||m==0||n<m)//特判
        printf("0\n");
    else{
        int a=n-m;
        int b=(m+1)/2;
        int res=calc(b-1,a+b-1);
        printf("%d\n",res);
    }
    return 0;
}

629. K个逆序对数组

1866. 恰有 K 根木棍可以看到的排列数目

629. K个逆序对数组

1 <= n <= 1000
0 <= k <= 1000

1866. 恰有 K 根木棍可以看到的排列数目

1 <= n <= 1000
1 <= k <= n

方法一:动态规划

f[i][j] 表示使用数字 1, 2, ⋯, i 的排列构成长度为 i 的数组,并且恰好包含 j 个逆序对的方案数。
边界条件为:
f[0][0] = 1,即不用任何数字可以构成一个空数组,它包含 0 个逆序对;
f[i][j<0] = 0,由于逆序对的数量一定是非负整数,因此所有 j < 0 的状态的值都为 0。
f [ i ] [ j ] = f [ i ] [ j − 1 ] − f [ i − 1 ] [ j − i ] + f [ i − 1 ] [ j ] f[i][j]=f[i][j−1]−f[i−1][j−i]+f[i−1][j] f[i][j]=f[i][j1]f[i1][ji]+f[i1][j]

class Solution:
    def kInversePairs(self, n: int, k: int) -> int:
        mod = 10**9 + 7
        
        f = [1] + [0] * k
        for i in range(1, n + 1):
            g = [0] * (k + 1)
            for j in range(k + 1):
                g[j] = (g[j - 1] if j - 1 >= 0 else 0) - (f[j - i] if j - i >= 0 else 0) + f[j]
                g[j] %= mod
            f = g
        
        return f[k]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值