斯特林(Stirling)数
斯特林数是组合数学中,关于子集划分和置换划分的一类问题的解。其中,分为第一类斯特林数和第二类斯特林数。
第一类斯特林数
[ n k ] {n \brack k} [kn] 表示 n n n个元素排成 k k k个轮换。这里的轮换是指:
例如存在轮换 [ 1 , 2 , 3 ] [1,2,3] [1,2,3],那么意思就是存在三个相等序列 [ 1 , 2 , 3 ] = [ 2 , 3 , 1 ] = [ 3 , 1 , 2 ] [1,2,3]=[2,3,1]=[3,1,2] [1,2,3]=[2,3,1]=[3,1,2]。
特别的,一个元素的轮换只有他本身,因为 [ 1 ] [1] [1]单个元素只能和自己轮换。两个元素的轮换也只有一个,因为 [ 1 , 2 ] = [ 2 , 1 ] [1,2]=[2,1] [1,2]=[2,1]是同一个轮换。
一般来说,对于 [ n 1 ] {n \brack 1} [1n],每一个 n n n个元素的全排列正好对应 n n n个轮换,因此根据除法原理:
[ n 1 ] = n ! n = ( n − 1 ) ! {n \brack 1} = \frac{n!}{n} = (n-1)! [1n]=nn!=(n−1)!
事实上,还容易看出:
[ n n ] = 1 {n \brack n} = 1 [nn]=1
[ n n − 1 ] = ( n 2 ) {n \brack n-1} = \binom{n}{2} [n−1n]=(2n)
对于最后一个等式,运用的鸽巢原理,相当于我们把 n n n个元素放进 n − 1 n-1 n−1个箱子里面,并且不能放空,那么必有两个元素在同一个箱子里面,因为有两个元素的那个箱子的轮换相等,所以我们不用做任何处理。
现在我们考虑第一类斯特林数的递推公式,考虑 n n n个元素最后一个元素 n n n,对于他的安放方式有两种。第一种自己单独成为一个轮换,方案数为 [ n − 1 k − 1 ] {n-1 \brack k-1} [k−1n−1]。第二种,插入前 n − 1 n-1 n−1个元素生成的 k k k个轮换中,方案数为 ( n − 1 ) [ n − 1 k ] (n-1){n-1 \brack k} (n−1)[kn−1](对于每一个轮换,假如这个轮换的大小为 ∣ S ∣ |S| ∣S∣,那么插入一个元素变成一个新的轮换有 ∣ S ∣ |S| ∣S∣种方式,即可以在每一个元素前面插入而不重复,因为 ∑ ∣ S i ∣ = ( n − 1 ) \sum |S_i|=(n-1) ∑∣Si∣=(n−1),故总共有 ( n − 1 ) (n-1) (n−1)种插入方式),因此,我们的递推公式为:
[ n k ] = [ n − 1 k − 1 ] + ( n − 1 ) [ n − 1 k ] {n \brack k} = {n-1 \brack k-1} + (n-1){n-1 \brack k} [kn]=[k−1n−1]+(n−1)[kn−1]
其中,我们认为:
[ 0 0 ] = 1 [ n 0 ] = 0 ( n > 0 ) {0 \brack 0} = 1 \\ {n \brack 0} = 0 (n \gt 0) [00]=1[0n]=0(n>0)
例题
看不到的火柴数
设答案为 P ( n , k ) P(n,k) P(n,k)
我们考虑最后一个位置能看到或者不能看到。如果能看到那么长度为 n n n的火柴必定在最后一个位置,方案数为 P ( n − 1 , k − 1 ) P(n - 1,k - 1) P(n−1,k−1)。如果看不到,那么最后一个位置上的元素可以是前 [ 1 , n − 1 ] [1,n-1] [1,n−1]内的所有元素和元素 n n n做置换,相对大小不变,因此方案数为 ( n − 1 ) ∗ P ( n − 1 , k ) (n-1)*P(n-1,k) (n−1)∗P(n−1,k)。因此递推公式为:
P ( n , k ) = P ( n − 1 , k − 1 ) + ( n − 1 ) ∗ P ( n − 1 , k ) P(n,k) = P(n - 1,k - 1) + (n-1)*P(n-1,k) P(n,k)=P(n−1,k−1)+(n−1)∗P(n−1,k)
答案就是第一类斯特林数。 P ( n , k ) = [ n k ] P(n,k) = {n \brack k} P(n,k)=[kn]
高楼
我们枚举最高建筑 n n n的位置,他前面可以有 [ 0 , n − 1 ] [0,n-1] [0,n−1]个建筑,故答案为:
a n s = ∑ i = 0 n − 1 [ i f − 1 ] [ n − i − 1 b − 1 ] ( n − 1 i ) ans = \sum_{i = 0}^{n-1} {i \brack f - 1} {n - i - 1 \brack b - 1} \binom{n - 1}{i} ans=i=0∑n−1[f−1i][b−1n−i−1](in−1)
查询次数很多,这么做必定会超时。因此我们必须使用一个神奇的公式化简这个和式。
具体的,我们把最高建筑 n n n放在最后面,构造 [ n − 1 f + b − 2 ] {n - 1 \brack f + b - 2} [f+b−2n−1],接着,然后把前面的 b − 1 b-1 b−1个圆排列移到后面去,因此答案为:
a n s = [ n − 1 f + b − 2 ] ( f + b − 2 b − 1 ) ans = {n - 1 \brack f + b - 2} \binom{f + b - 2}{b - 1} ans=[f+b−2n−1](b−1f+b−2)
这样,我们得到了一个很重要的公式:
[ n l + m ] ( l + m l ) = ∑ k [ k l ] [ n − k m ] ( n k ) {n \brack l + m} \binom{l + m}{l} = \sum_k {k \brack l}{n-k \brack m} \binom{n}{k} [l+mn](ll+m)=k∑[lk][mn−k](kn)
第二类斯特林数
第二类斯特林数 { n m } {n \brace m} {mn}是指,将 n n n个不同的元素划分为 m m m个相同的非空集合的方案数。
有如下递推式子:
{ n m } = { n − 1 m − 1 } + { n − 1 m } × m {n \brace m} = {n-1 \brace m-1} + {n-1 \brace m} \times m {mn}={m−1n−1}+{mn−1}×m
我们考虑最后一个元素的划分,第一种方式将最后一个元素单独放在一个集合中,需要将前 n − 1 n-1 n−1个元素划分成 m − 1 m-1 m−1个集合。第二种情况将最后一个元素插入到之前已经划分好的集合中,有 m m m中插入方式,插入到每个集合都是不同的,方案数为 { n − 1 m } × m {n-1 \brace m} \times m {mn−1}×m。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define FR freopen("in.txt", "r", stdin)
#define FW freopen("out.txt", "w", stdout)
struct Decimal
{
int bits[500];
int tot;
Decimal() : Decimal(0)
{
}
Decimal(int init) : tot(0)
{
fill(bits, bits + 500, 0);
if (init == 0)
{
tot = 1;
return;
}
for (; init; init /= 10)
{
bits[tot++] = init % 10;
}
}
Decimal operator+(Decimal o)
{
Decimal ans(0);
int i = 0;
for (; i < max(tot, o.tot); i++)
{
ans.bits[i] += bits[i] + o.bits[i];
ans.bits[i + 1] += ans.bits[i] / 10;
ans.bits[i] %= 10;
}
ans.tot = ans.bits[i] == 0 ? i : i + 1;
return ans;
}
Decimal operator*(Decimal o)
{
Decimal ans(0);
for (int i = 0; i < tot; i++)
{
for (int j = 0; j < o.tot; j++)
{
ans.bits[i + j] += bits[i] * o.bits[j];
ans.bits[i + j + 1] += ans.bits[i + j] / 10;
ans.bits[i + j] %= 10;
}
}
int t = tot + o.tot - 1;
while (ans.bits[t] == 0)
t--;
ans.tot = t + 1;
return ans;
}
Decimal operator*(int k)
{
Decimal fact(k);
return (*this) * fact;
}
void output()
{
for (int i = tot - 1; i >= 0; i--)
{
putchar(bits[i] + '0');
}
putchar('\n');
}
};
Decimal stir[105][105];
int main()
{
stir[0][0] = Decimal(1);
for (int n = 1; n <= 101; n++)
{
for (int m = 1; m <= min(101, n); m++)
{
stir[n][m] = stir[n - 1][m - 1] + stir[n - 1][m] * m;
}
}
int N, M;
while (scanf("%d %d", &N, &M) != EOF)
{
stir[N][M].output();
}
return 0;
}