本博客包含:
组合数、二项式定理之类的东西不会在本文科普。因为比较基础。
第一类斯特林数
基础
定义:n 个数组成 k 个圆排列的方案数,为 n,k 的第一类斯特林数。记作 [kn]。
第一类斯特林数的递推式:
[kn]=[k−1n−1]+[kn−1](n−1)
意义为:第 n 个数可以单独形成一个圆排列,也可以选择前 n−1 个数的任意一个并放到它后面。
还有另一个递推式:
[kn]=i=1∑n[k−1n−i](i−1n−1)(i−1)!
意义为:枚举 1 所在的圆排列大小 i,剩下的数字组成圆排列方案数为 [k−1n−1],选择除了 1 之外的其他数到 1 所在圆排列的方案数为 (i−1n−1),这些数字组成圆排列的方案数为 (i−1)!。
当然有的时候也需要把斯特林数拆开,那么这个递推式是经常反过来用的。
根据斯特林数的定义,显然,同一行所有斯特林数的和为 n!。证明则考虑枚举长度为 n 的排列的循环数即可。
n 个楼排成一行,高度是 n 的排列。求有多少种方案满足从左看恰好能看到 a 个楼,从右看恰好能看到 b 个楼。
多组数据,T≤2×105,n≤5×104,a,b≤100。
题解
最高的建筑无论左右都一定能被看到,且挡住了后面所有的视线,以此为契机。
考虑把所有的建筑分为 a+b−1 个“建筑群”,每个建筑群中选出最高的建筑为代表,放在这个建筑群的最左边,挡住建筑群中其它的建筑。
如果大小为 k 的建筑群代表已经确定了,剩下的建筑怎么放无所谓。有 (k−1)! 种放的方案数值上等于一个圆排列。
除去最高的建筑后,从左边看有 a−1 个代表,从右有 b−1 个。一个代表为一个群。于是变成了将 n−1 个建筑划分为 a+b−2 个建筑群,然后选择其中 a−1 个放在左边:
ans=(a−1a+b−2)[a+b−2n−1]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| #include<bits/stdc++.h> #define int long long using namespace std; const int max_n=50004,max_m=202,mod=1000000007; inline int read(){ int x=0;bool w=0;char c=getchar(); while(c<'0' || c>'9') w|=c=='-',c=getchar(); while(c>='0' && c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar(); return w?-x:x; } inline void write(int x){ if(x<0) putchar('-'),x=-x; if(x>9) write(x/10); putchar(x%10^48); }
const int N=50000,M=200; int n,a,b,C[max_m][max_m],S[max_n][max_m];
signed main(){ C[0][0]=S[0][0]=1; for(register int i=1;i<=M;++i){ C[i][0]=C[i][i]=1; for(register int j=1;j<i;++j) C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod; } for(register int i=1;i<=N;++i) for(register int j=1;j<=M;++j) S[i][j]=(S[i-1][j-1]+S[i-1][j]*(i-1))%mod; for(register int T=read();T>0;--T){ n=read(),a=read(),b=read(); write(C[a+b-2][a-1]*S[n-1][a+b-2]%mod),putchar('\n'); } return 0; }
|
第一类斯特林数 行
如何快速求出一行的第一类斯特林数呢?显然,n2 求第一类斯特林数还是有些慢的。我们需要找到其更多的性质。
xn=i=0∑n[in]xi
证明
设生成函数 Fn(x)=∑i{in}xi,由递推式得到 Fn=xFn−1+(n−1)Fn−1=(x+n−1)Fn−1。
下面考虑如何求 xn,考虑倍增,已知 xn 求 x2n=xn(x+n)n。换句话说,给定 f(x),求 f(x+n):
f(x+n)=i=0∑nfi(x+n)i
二项式定理:
i=1∑nfij=1∑ixjni−j(ji)
交换 sigma,拆组合数:
j=1∑njxki=j∑ni!fi(i−j)!ni−j
这是一个卷积形式,NTT 即可。复杂度 nlogn。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| inline void sol(int len){ if(len==1){ f[1]=1; return; } sol(len>>1); int n=len>>1,tmp=1; while(tmp<=n+n) tmp<<=1; for(register int i=0;i<=n;++i) a[i]=f[i]*fac[i]%mod; for(register int i=0,base=1;i<=n;++i,base=base*n%mod) b[i]=inv[i]*base%mod; for(register int i=0,j=n;i<j;++i,--j) swap(b[i],b[j]); for(register int i=n+1;i<tmp;++i) a[i]=b[i]=0; DXS::Mul(a,b,n,n);
for(register int i=0;i<=n;++i) a[i]=a[i+n]*inv[i]%mod; for(register int i=n+1;i<=n*2;++i) a[i]=0; DXS::Mul(f,a,n,n); if(len&1){ for(register int i=n*2+1;i>=1;--i) f[i]=(f[i-1]+(len-1)*f[i]%mod)%mod; f[0]=f[0]*(len-1)%mod; } }
|
求有多少种大小为 n 的排列,满足其恰好有 a 个前缀最大值和 b 个后缀最大值。
a,b,n≤105。对 998244353 取模。
多组数据,T≤2×105,n≤5×104,a,b≤100。
题解
如果一个数比它左边一个数小,它就不会是前缀最大值。也就是说一个数是前缀最大值当且仅当它左边没有数比它大。
这和上一道例题“从左边看到这个建筑”的要求是同理的。因此这个题的做法和上一个题一模一样:
ans=(a−1a+b−2)[a+b−2n−1]
注意到数据范围变了,需要用 NTT 求出第 n 行斯特林数。
多项式板子的代码会省略。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| const int N=200000; int n,A,B,fac[max_n],inv[max_n];
inline int C(int n,int m){ if(n<m || n<0 || m<0) return 0; return fac[n]*inv[m]%mod*inv[n-m]%mod; }
int f[max_n*8],a[max_n*8],b[max_n*8];
inline void sol(int len){ if(len==1){ f[1]=1; return; } sol(len>>1); int n=len>>1,tmp=1; while(tmp<=n+n) tmp<<=1; for(register int i=0;i<=n;++i) a[i]=f[i]*fac[i]%mod; for(register int i=0,base=1;i<=n;++i,base=base*n%mod) b[i]=inv[i]*base%mod; for(register int i=0,j=n;i<j;++i,--j) swap(b[i],b[j]); for(register int i=n+1;i<tmp;++i) a[i]=b[i]=0; DXS::Mul(a,b,n,n);
for(register int i=0;i<=n;++i) a[i]=a[i+n]*inv[i]%mod; for(register int i=n+1;i<=n*2;++i) a[i]=0; DXS::Mul(f,a,n,n); if(len&1){ for(register int i=n*2+1;i>=1;--i) f[i]=(f[i-1]+(len-1)*f[i]%mod)%mod; f[0]=f[0]*(len-1)%mod; } }
signed main(){ n=read(),A=read(),B=read(),fac[0]=inv[0]=1; if(n==1){ if(A+B==2) write(1); else write(0); return 0; } for(register int i=1;i<=N;++i) fac[i]=fac[i-1]*i%mod; inv[N]=mi(fac[N]); for(register int i=N-1;i>0;--i) inv[i]=inv[i+1]*(i+1)%mod; int tmp=1; while(tmp<=n) tmp<<=1; DXS::init(tmp); sol(n-1); write(C(A+B-2,A-1)*f[A+B-2]%mod); return 0; }
|
第一类斯特林数 列
首先我们需要知道上升幂到下降幂的转化:
xn=(−1)n(−x)n
根据第一类斯特林数的性质:
xn=i=0∑n[in]xi
把 x 用 −x 代进去:
(−1)nxn=i=0∑n[in](−x)i
因此有:
xn=i=0∑n[in](−1)n−ixi
下面考虑一个式子 (x+1)t,将其用二项式定理拆开,然后展开组合数:
(x+1)t=i=0∑i!tixi
上面有一个下降幂,转化成斯特林数,然后交换 ∑:
j=0∑tji=j∑(−1)i−j[ji]i!xi
这个式子先放着,把原来的 (x+1)t 换一种方式展开:
(x+1)t=exp[tln(x+1)]
然后泰勒:
i=0∑tii!lni(x+1)
两个式子放在一起:
j=0∑tji=j∑(−1)i−j[ji]i!xi=i=0∑tii!lni(x+1)
然后你会发现……
i=m∑(−1)i−m[mi]i!xi=m!lnm(x+1)
多项式 ln 和快速幂做到 nlogn。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| int n,k,fac[max_n],inv[max_n]; int f[max_n*8],g[max_n*8];
inline void sol(){ int tmp=1; while(tmp<=n) tmp<<=1; DXS::init(tmp);
g[0]=g[1]=1; DXS::Ln(g,f,tmp), DXS::Pow(f,g,n,k);
for(register int i=0;i<=n;++i) f[i]=g[i]*inv[k]%mod*fac[i]%mod*((i-k)&1?mod-1:1)%mod; }
signed main(){ n=read(),k=read(),N=n; if(n<k){ for(register int i=0;i<=n;++i) write(0),putchar(' '); return 0; } fac[0]=inv[0]=1; for(register int i=1;i<=n;++i) fac[i]=fac[i-1]*i%mod; inv[n]=mi(fac[n]); for(register int i=n-1;i>0;--i) inv[i]=inv[i+1]*(i+1)%mod; sol(); for(register int i=0;i<=n;++i) write(f[i]),putchar(' '); return 0; }
|
第二类斯特林数
基础
定义:把 n 个不同的球放到 m 个相同的盒子里,盒子不能为空的方案数。记为 {mn}。
根据定义有第二类斯特林数的递推式:
{mn}={m−1n−1}+m{mn−1}
意义为:考虑第 n 个球怎么放,要么放在一个已有的盒子里,要么新开一个。
有通项公式:
{mn}=m!1i=0∑m(−1)i(im)(m−i)n
证明考虑容斥,枚举至少多少盒子为空,在 m 里选出 i 个盒子为空,剩下的 n 个球随意分配。注意到这样计算会认为盒子之间是不同的,因此最后要除掉一个 m!。
第二类斯特林数还有一个奇妙的性质:
xn=i=0∑n(ix){in}i!=i=0∑x{in}xi
证明
第二个等式显然(把组合数拆开,然后把阶乘约掉),考虑第一个等式。求其组合意义:n 个不同的球放入 m 个不同的个盒子的方案数,看作枚举有多少个盒子非空,然后把 n 个球放入的方案数的和,因为第二类斯特林数的盒子是相同的,所以还要乘上 i!。
给定 n 个物品,每个物品有权值 wi,定义一个集合 S 的权值为 W(S)=∣S∣∑x∈Swx。
定义一个划分的权值为其划分出的所有集合权值的和。
求把 n 个物品划分为 k 个集合的所有方案的权值和,对 109+7 取模。
n≤2×105。
题解
考虑计算集合贡献时 ∣S∣ 表示啥,其实就是每个物品会对系数产生 1 的贡献。而划分好集合后,每个点都对当前点有 wi 的贡献。
钦定一个点,则每个点对自己的贡献是 {mn},而一个点对其他点的贡献是:把其他点划分好集合,则这个点对 i 的贡献是把自己和 i 放在一个集合里,也就是 {kn−1}。因此:
ans=[{kn}+(n−1){kn−1}]∑wi
1 2 3 4 5 6 7 8 9 10 11 12 13
| namespace sol1{ inline int S(int n,int m){ int res=0; for(register int i=0;i<=m;++i){ res+=C(m,i)*mi(m-i,n)%mod*(i&1?-1:1), res=(res+mod)%mod; } return res*inv[m]%mod; } inline void sol(){ write((S(n,k)+(n-1)*S(n-1,k)%mod)%mod*sumw%mod); } }
|
第二类斯特林数 行
由通项公式:
{mn}=m!1i=0∑m(−1)i(im)(m−i)n
把组合数拆开,约掉 m!:
i=0∑mi!(−1)i⋅(m−i)!(m−i)n
这就是很明显的卷积形式了,NTT 即可。复杂度 nlogn。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| int fac[max_n],inv[max_n]; int f[max_n*8],g[max_n*8];
signed main(){ n=read(),fac[0]=inv[0]=1; for(register int i=1;i<=n;++i) fac[i]=fac[i-1]*i%mod; inv[n]=mi(fac[n]); for(register int i=n-1;i>0;--i) inv[i]=inv[i+1]*(i+1)%mod; int tmp=1; while(tmp<=n) tmp<<=1; DXS::init(tmp); for(register int i=0;i<=n;++i){ f[i]=mi(i,n)*inv[i]%mod, g[i]=(i&1?mod-inv[i]:inv[i]); } DXS::Mul(f,g,n+1,n+1); for(register int i=0;i<=n;++i) write(f[i]),putchar(' '); return 0; }
|
给定 n,k,求所有 n 个点的带标号简单无向图的价值和对 998244353 取模的结果。一个图的价值为每个点度数的 k 次方和。
n≤109,k≤2×105。
题解
枚举一个点的度:
ans=ni=0∑n−1ik⋅22(n−1)(n−2)(in−1)
即,这个度的贡献为 ik,达到这个度需要在 n−1 条可行的边里选 i 条链出去。剩下的边随意。
把 2 的幂提出去,只考虑里面的部分,用第二类斯特林数把 ik 拆开:
i=0∑n−1(in−1)j=0∑min(i,k)(ji){jk}j!
交换 sigma:
j=0∑min(n−1,k){jk}j!i=j∑n−1(ji)(in−1)
组合恒等式:
j=0∑min(n−1,k){jk}j!i=j∑n−1(i−jn−j−1)
后面的东西显然是 2n−j−1。因此答案为:
ans=22(n−1)(n−2)ni=0∑min(n−1,k)i!{ik}(in−1)2n−i−1
求第 k 行的第二类斯特林数即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| int n,k,N,fac[max_n],inv[max_n]; int f[max_n*8],g[max_n*8],ans,c[max_n];
inline int C(int n,int m){ if(n<m || n<0 || m<0) return 0; return fac[n]*inv[m]%mod*inv[n-m]%mod; }
inline void sol(){ int tmp=1; while(tmp<=k) tmp<<=1; DXS::init(tmp); for(register int i=0;i<=k;++i){ f[i]=mi(i,k)*inv[i]%mod, g[i]=(i&1?mod-inv[i]:inv[i]); } DXS::Mul(f,g,k+1,k+1); }
signed main(){ n=read(),k=read(),N=min(n-1,k),fac[0]=inv[0]=1; for(register int i=1;i<=k;++i) fac[i]=fac[i-1]*i%mod; inv[k]=mi(fac[k]); for(register int i=k-1;i>0;--i) inv[i]=inv[i+1]*(i+1)%mod; c[0]=mi(2,n-1); for(register int i=1;i<=N;++i) c[i]=c[i-1]*inv[2]%mod*(n-i)%mod; sol();
for(register int i=0;i<=N;++i){ ans+=f[i]%mod*c[i]%mod, ans%=mod; } write(ans*n%mod*mi(2,((n-1)*(n-2)/2)%(mod-1))%mod); return 0; }
|
第二类斯特林数 列
考虑第二类斯特林数的组合意义,如果把“相同的盒子”换成“不同的盒子”,只要让它乘上 m! 即可,因此考虑让 n 个不同小球放入 k 个不同盒子的方案数。
直接 EGF,设方案数的 EGF 为 F,单个盒子的 EGF 为 G,则 F=Gk,G=∑i=1ni!xi。
因此有:
k!i=0∑{ki}i!xi=F
多项式快速幂即可,复杂度 nlogn。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| int n,k,fac[max_n],inv[max_n]; int f[max_n*8],g[max_n*8];
inline void sol(){ int tmp=1; while(tmp<=n) tmp<<=1; DXS::init(tmp); for(register int i=1;i<=n;++i) g[i]=inv[i]; DXS::Pow(g,f,n,k); for(register int i=0;i<=n;++i) f[i]=f[i]*fac[i]%mod*inv[k]%mod; }
signed main(){ n=N=read(),k=read(); if(n<k){ for(register int i=0;i<=n;++i) write(0),putchar(' '); return 0; } fac[0]=inv[0]=1; for(register int i=1;i<=n;++i) fac[i]=fac[i-1]*i%mod; inv[n]=mi(fac[n]); for(register int i=n-1;i>0;--i) inv[i]=inv[i+1]*(i+1)%mod; sol(); for(register int i=0;i<=n;++i) write(f[i]),putchar(' '); return 0; }
|
斯特林反演
求一列第一类斯特林数,我们用上升幂到下降幂的转化,结合第一类斯特林数的性质,得到了这样的式子:
xn=i=0∑n[in](−1)n−ixi
第二类斯特林数里有这样的性质:
xn=i=0∑x{in}xi
根据下降幂转上升幂 xn=(−1)n(−x)n:
xn=i=0∑n{in}(−1)n−ixi
结合第一类斯特林数的另一个性质联立:
⎩⎪⎪⎪⎪⎨⎪⎪⎪⎪⎧xn=∑i=0n{in}xixn=∑i=0n[in]xixn=∑i=0n{in}(−1)n−ixixn=∑i=0n[in](−1)n−ixi
把第二个套进第三个:
xn=i=0∑n{in}(−1)n−ij=0∑i[ji]xj
交换 ∑,分离出 j=n 的一项:
xn=xn+j=0∑n−1xji=j∑n(−1)n−i{in}[ji]
因此:
i=j∑n(−1)n−i{in}[ji]=0
类似地,有:
i=m∑n(−1)m+i or n+i{in}[mi]=[n=m]
i=m∑n(−1)m+i or n+i[in]{mi}=[n=m]
这两个公式被称为反转公式。而斯特林反演的一个常用公式,就是取 m=1 的特例:
i=1∑n(−1)i+1{in}(i−1)!=[n=1]
当然还有另一种形式,设 f(n)=∑i=0n{in}g(i),则:
g(n)=i=0∑n(−1)n−i[in]f(i)
证明为直接代入,然后发现和反转公式的推倒步骤类似。
定义两个点数相同的图的异或为一个新的图,其中在原来两个图中出现次数和为 1 的边在新图中,其它边不在新图中。
给定 s 个点数均为 n 的图,求有多少种选择若干个图的方案,使得这些图的异或是一个连通图。
2≤n≤10,s≤60。
题解
直接考虑连通这个条件不太好作,考虑连通块个数。n 很小,直接枚举点的子集划分。将其加入到 m 个子集内,强制不同点集的点不连边,相同点集的随意连。设按这种方法划分的方案数为 fm,最后恰好形成 m 个连通块的方案数为 gm。则 fm 相当于在 gk(k≥m) 种选择 m 个集合合并:
fm=i=m∑n{mi}gi
斯特林反演:
gm=i=m∑n(−1)i−m[mi]fi
整个连通图连通的方案数为 g1:
ans=i=1∑n(−1)i−1(i−1)!fi
n 很小,复杂度不成问题,考虑怎么求 f。
仍然枚举子集划分,考虑一个图的选择情况(选或不选)对应了一个未知数(0 或 1)。那么对于每一条确定有或者没有的边,我们都可以列出一个异或方程,形成异或方程组。而对于一个未知数 x 如果 x 为主元,则必须在或不在子集里,若 x 为自由元,则在或不在都可以。
设 cnt 为自由元数量,则这种子集划分产生的贡献为 2cnt。因为是异或方程组,求自由元数量可以线性基解决。复杂度 O(n2sBn),B 为贝尔数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| #include<bits/stdc++.h> #define int long long using namespace std; const int max_n=66; inline int read(){ int x=0;bool w=0;char c=getchar(); while(c<'0' || c>'9') w|=c=='-',c=getchar(); while(c>='0' && c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar(); return w?-x:x; } inline void write(int x){ if(x<0) putchar('-'),x=-x; if(x>9) write(x/10); putchar(x%10^48); }
struct LB{ int p[max_n],n,cnt; inline void init(int x){ n=x,cnt=0; memset(p,0,sizeof(p)); } inline void insert(int x){ for(register int i=n;i>=0;--i) if(x&(1LL<<i)){ if(!p[i]){ ++cnt,p[i]=x; return; } x^=p[i]; } } inline int askcan(){ return (1LL<<cnt); } inline int askcant(){ return (1LL<<(n-cnt)); } }lb;
int n,s,ans,fac[max_n];
int col[max_n],f[max_n]; bool e[max_n][max_n][max_n];
inline void dfs(int l,int m){ if(l>n){ lb.init(s); for(register int i=1;i<=n;++i) for(register int j=i+1;j<=n;++j){ if(col[i]==col[j]) continue; int x=0; for(register int k=0;k<s;++k) if(e[k][i][j]) x|=(1LL<<k); lb.insert(x); } f[m]+=lb.askcant(); return; } for(register int i=1;i<=m;++i){ col[l]=i; dfs(l+1,m); } col[l]=m+1; dfs(l+1,m+1); }
signed main(){ s=read(); for(register int id=0;id<s;++id){ string p; cin>>p;int tmp=p.size(); for(register int i=2;i<=10;++i) if(i*(i-1)/2==tmp){ n=i; break; } for(register int i=1,k=0;i<=n;++i) for(register int j=i+1;j<=n;++j,++k) if(p[k]=='1') e[id][i][j]=1; } fac[0]=1; for(register int i=1;i<=n;++i) fac[i]=fac[i-1]*i; dfs(1,0); for(register int i=1;i<=n;++i){ ans+=f[i]*fac[i-1]*(i&1?1:-1); } write(ans); return 0; }
|