斯特林数

好难学哦qwq md我还没写完

以dzyo ppt为基础

第一类斯特林数

\begin{bmatrix} n\\m \end{bmatrix},也写作s(n,m),表示将n个数放到m个环里面去。环旋转同构。

递推式很好想。下一个数放在原来的环里还是新开一个。

s(n,m)=(n-1)s(n-1,m)+s(n-1,m-1)

边界\begin{bmatrix} 0\\0 \end{bmatrix}=1

性质

1.s(n,1)=(n-1)!很明显。

2.s(n,2)=(n-1)!\sum_{i=1}^{n-1}\frac{1}{i}

数学归纳法证明:

s(n,2)=s(n-1,1)+(n-1)s(n-1,2)=(n-2)!+(n-1)(n-2)!\sum_{i=1}^{n-2}\frac{1}{i}=(n-2)!+(n-1)!\sum_{i=1}^{n-2}\frac{1}{i}=(n-1)!\sum_{i=1}^{n-1}\frac{1}{i}

其实就是提了个公因式罢了。

3.\sum_{i=0}^ns(n,i)=n!

使用置换的思想。首先n个数的排列有n!个。

并且任何一个排列对应了一个置换。而置换会产生环,因为置换和排列一一对应,所以我们只需要求n个数能产生所有可能环树的方案和就可以。

也就是上面的公式。因为s(n,0)=0,所以把i=0加进去也无可厚非。

第二类斯特林数

这东西就很毒瘤了。

\begin{Bmatrix} n\\m \end{Bmatrix}=S(n,m)

表示将n个元素放到m个集合中。集合不能为空。

继续枚举下一个元素是否新开集合,得到

S(n,m)=mS(n-1,m)+S(n-1,m-1)

通项公式

S(n,m)=\frac{1}{m!}\sum_{k=0}^n(-1)^kC_m^k(m-k)^n

正确的解释是二项式反演,如何反演在下面的下面的下面。通过这个式子:

由于第二类斯特林数可以理解为将n个球刷上x种颜色,我们还可以通过枚举颜色数来计算x^n

x^n=\sum_{k=1}^x\binom{x}{k}\frac{1}{k!}\begin{Bmatrix} n\\k \end{Bmatrix}

博主写到后面才反应过来这个公式是反演出来的。事实上我根本不明白为什么在下面的式子中集合起初是有顺序的。

警告:下面的解释很感性,可能是错的

如果没有非空的限制,答案是m^n

现在非空,那我们枚举有k个集合是空的。但很明显,2个集合是空的包含了1个集合是空的。所以容斥一下。

从m个集合中选出k个集合,剩下的集合随便放。但这个时候我们眼中的集合是有顺序的,所以最后要除以m!来消去顺序性。

这样我们可以很快的求出一行的第二类斯特林数。因为这是一个卷积形式。

emmm因为我太菜不太会所以请教了一下巨神,这里来说一下怎么卷积,,

原式等于\sum_{k=0}^n(-1)^k\frac{1}{k!(m-k)!}(m-k)^n=\sum_{k=0}^n\frac{(-1)^k}{k!}\frac{(m-k)^n}{(m-k)!}

a_i=\frac{(-1)^i}{i!},b_i=\frac{i^n}{i!},然后将a和b都生成函数表示,卷积起来的第m项就是原式。

或者说,卷起来的每一项系数对应了这一行二类斯特林的一项。

//5395 Luogu
#include<bits/stdc++.h>
using namespace std;
#define in read()
#define int long long
int in{
	int cnt=0,f=1;char ch=0;
	while(!isdigit(ch)){
		ch=getchar();if(ch=='-')f=-1;
	}
	while(isdigit(ch)){
		cnt=cnt*10+ch-48;
		ch=getchar();
	}return cnt*f;
}
const int mod=167772161;
int n;
int fac[200003],ifac[200003];
int A[1000003],b[1000003],limit,l,r[1000003];
int ksm(int x,int y){
	int sum=1;
	while(y){
		if(y&1)sum=sum*x%mod;y>>=1;x=x*x%mod;
	}return sum;
}
void getl(int len){
	limit=1,l=0;
	while(limit<=len)limit<<=1,l++;
	for(int i=0;i<limit;i++){
		r[i]=(r[i>>1]>>1)|((i&1)<<(l-1));
	}
}
void NTT(int *a,int len,int inv){
	for(int i=0;i<len;i++){
		if(i<r[i])swap(a[i],a[r[i]]);
	}
	for(int mid=1;mid<len;mid<<=1){
		int wn=ksm(3,(mod-1)/(mid<<1));
		for(int i=0;i<len;i+=mid*2){
			int omega=1;
			for(int j=0;j<mid;j++,omega=omega*wn%mod){
				int x=a[i+j],y=a[i+j+mid]*omega%mod;
				a[i+j]=(x+y)%mod;a[i+j+mid]=(x-y+mod)%mod; 
			}
		}
	}if(inv==1)return;
	reverse(a+1,a+len);int gu=ksm(len,mod-2);
	for(int i=0;i<len;i++)a[i]=a[i]*gu%mod;
}
void solve(){
	for(int i=0;i<=n;i++)A[i]=((i&1)?(mod-1):1)*ifac[i]%mod,b[i]=ifac[i]*ksm(i,n)%mod;
	//for(int i=0;i<=n;i++)cout<<A[i]<<" ";cout<<endl;for(int i=0;i<=n;i++)cout<<b[i]<<" ";cout<<endl;
	getl(n<<1);
	NTT(A,limit,1);NTT(b,limit,1);
	//for(int i=0;i<limit;i++)cout<<A[i]<<" ";cout<<endl;for(int i=0;i<limit;i++)cout<<b[i]<<" ";cout<<endl;
	for(int i=0;i<limit;i++)A[i]=A[i]*b[i]%mod;
	NTT(A,limit,-1);
	for(int i=0;i<=n;i++)cout<<A[i]<<" ";
}	
signed main(){
	n=in;fac[0]=ifac[0]=1;
	for(int i=1;i<=n;i++){
		fac[i]=fac[i-1]*i%mod;
	}
	ifac[n]=ksm(fac[n],mod-2);//cout<<fac[n]<<" "<<ifac[n]<<endl;
	for(int i=n-1;i>=1;i--)ifac[i]=ifac[i+1]*(i+1)%mod;
	//for(int i=0;i<=n;i++)cout<<fac[i]<<" ";cout<<endl;
	//for(int i=0;i<=n;i++)cout<<ifac[i]<<" ";cout<<endl;
	solve();
	return 0;
}

好久没打NTT,忘了两个东西卷积起来项数要变大,,getl的时候爆锅了。

转化幂

其实这个用的更多。

x^n=\sum_{k=1}^n\begin{Bmatrix} n\\k \end{Bmatrix}x^{\underline k}

数学归纳法证明

x^n=x*x^{n-1}=x*\sum_{k=1}^{n-1}\begin{Bmatrix} n-1\\k \end{Bmatrix}x^{\underline k}=\sum_{k=1}^{n-1}\begin{Bmatrix} n-1\\k \end{Bmatrix}(x^{\underline {k+1}}+kx^{\underline k})=\sum_{k=1}^n\begin{Bmatrix} n-1\\k-1 \end{Bmatrix}x^{\underline k}+k\sum_{k=1}^n\begin{Bmatrix} n-1\\k \end{Bmatrix}x^{\underline k}=\sum_{k=1}^n\begin{Bmatrix} n\\k \end{Bmatrix}x^{\underline k}

最关键的就是把那个多出来的x拆成k和(x-k)然后分别乘进去。这种思想我称为下降幂补阶。最后直接套递推式。

与此同时,也有上升幂的说法。

x^n=\sum_{k=1}^n(-1)^{n-k}\begin{Bmatrix} n\\k \end{Bmatrix}x^{\overline k}

这次我终于可以自己推证明了,,

x^n=x*x^{n-1}=x*\sum_{k=1}^{n-1}(-1)^{n-k-1}\begin{Bmatrix} n-1\\k \end{Bmatrix}x^{\overline k}=\sum_{k=1}^{n-1}(-1)^{n-k-1}\begin{Bmatrix} n-1\\k \end{Bmatrix}x^{\overline {k+1}}+\sum_{k=1}^{n-1}k(-1)^{n-k}\begin{Bmatrix} n-1\\k \end{Bmatrix}x^{\overline k}=\sum_{k=1}^n(-1)^{n-k}\begin{Bmatrix} n-1\\k-1 \end{Bmatrix}x^{\overline k}+\sum_{k=1}^nk(-1)^{n-k}\begin{Bmatrix} n-1\\k \end{Bmatrix}x^{\overline k}=\sum_{k=1}^n(-1)^{n-k}\begin{Bmatrix} n\\k \end{Bmatrix}x^{\overline k}

然后通过第一类斯特林还可以将上升\下降幂改成幂次方。

x^{\overline n}=\sum_{k=1}^n\begin{bmatrix} n\\k \end{bmatrix}x^kx^{\underline n}=\sum_{k=1}^n\begin{bmatrix} n\\k \end{bmatrix}(-1)^{n-k}x^k

仍然数学归纳

x^{\overline n}=(x+n-1)*x^{\overline{n-1}}=\sum_{k=1}^{n-1}\begin{bmatrix} n-1\\k \end{bmatrix}x^k(x+n-1)

x^{\underline n}=(x-n+1)*x^{\underline {n-1}}=\sum_{k=1}^{n-1}\begin{bmatrix} n-1\\k \end{bmatrix}(-1)^{n-k-1}*(x-n+1)

你可以发现乘x进去相当于一起向高次挪一位,就变成了递推公式的第一项。(n-1)乘进去跟递推公式第二项是一样的。latex太难写我就不打证明了。

由此我们还可以得出第一类斯特林的一个快速求法。

令生成函数F(n)=\sum_{k=0}^{inv}\begin{bmatrix} n\\k \end{bmatrix}x^k

根据我们刚刚说的递推公式转换得到F(n)=(x+n-1)F(n-1)

所以F(n)=x^{\overline n}

分治FFT做已经很快了。如果还是rilong可以倍增FFT。虽然我不会,看dzyo ppt去。(md洛谷卡log方只能倍增我还不会)

与此同时。由于第二类斯特林数可以理解为将n个球刷上x种颜色,我们还可以通过枚举颜色数来计算x^n

x^n=\sum_{k=1}^x\binom{x}{k}\frac{1}{k!}\begin{Bmatrix} n\\k \end{Bmatrix}

在x种颜色中选k个。然后消去环之间的顺序性,最后n个球随便选择。

这个式子有组合数,所以我们可以二项式反演。诶,我还没学二项式反演耶。

于是我去学了二项式反演然后把博客搬了进来。

如果感到高兴你就拍拍手,如果没有学过你就点点我

x^n=\sum_{k=1}^x\binom{x}{k}k!\begin{Bmatrix} n\\k \end{Bmatrix}

所以x!\begin{Bmatrix} n\\x \end{Bmatrix}=\sum_{i=0}^x(-1)^{x-i}\binom{x}{i}i^n

所以,,继续按照刚刚的那种分类方式做卷积,nlogn。

 

哎呀我忘了写斯特林反演TAT那还例个什么题

 

一个小小的例题 TCO14 Wildcard CountTables

一个n*m的矩阵,c种颜色,每个格子可以染色,但任何两行或者两列不能完全相同。求方案数。

我们可以先确定行不相同,然后再删掉列相同。

首先行不相同的方案数为(c^m)^{\underline n},即随便染色,然后每行依次-1。

然后设f(i)表示i列互不相同的方案数。

那我们用总方案悄悄地容斥一下就可以得到了

f(m)=(c^m)^{\underline n}-\sum_{i=1}^{m-1}\begin{Bmatrix} m\\i \end{Bmatrix}f(i)

那就随便预处理阶乘啊之类的就星了

另一个小小的例题 FJOI2016建筑师

不管怎么样,肯定会看见最高的那个。

那去掉最高的那个,我们左边看到(x-1)个,右边(y-1)个。

而那些被挡住的小楼的不同也会导致方案不同。

所以左边有(x-1)个环,右边(y-1)个。共(x+y-2)个环,其中有(x-1)个放左边。一类斯特林枚举最高楼位置即可。

Ans=\begin{bmatrix} n-1\\x+y-2 \end{bmatrix}\binom{x+y-2}{x-1}

// luogu-judger-enable-o2
#include<bits/stdc++.h>
#define int long long
using namespace std;
int s[50003][205],c[205][205];
int t,n,a,b;
const int p=1e9+7;
signed main(){
	s[0][0]=c[0][0]=1;
	for(int i=1;i<=50000;i++){
		for(int j=1;j<=200;j++){
			s[i][j]=(s[i-1][j-1]+(i-1)*(s[i-1][j])%p)%p;
		}
	}
	for(int i=1;i<=202;i++){
		c[i][0]=1;
		for(int j=1;j<=202;j++){
			c[i][j]=(c[i-1][j]+c[i-1][j-1])%p;
		}
	}
	cin>>t;
	while(t--){
		cin>>n>>a>>b;
		int ans=(s[n-1][a+b-2]*c[a+b-2][a-1])%p;
		cout<<ans<<endl;
	}
	return 0;
}

还有一个小小的例题 TJOI\HEOI2016 求和

\sum_{i=0}^n\sum_{j=0}^i\begin{Bmatrix} i\\j \end{Bmatrix}2^jj!

两种做法。我选择第一种。

1.通项公式强行使用。

因为j>i时,二类为0,所以改成n无妨。

\sum_{i=0}^n\sum_{j=0}^n\begin{Bmatrix} i\\j \end{Bmatrix}2^jj!=\sum_{j=0}^n2^jj!\sum_{i=0}^n\begin{Bmatrix} i\\j \end{Bmatrix}=\sum_{j=0}^n2^jj!\sum_{i=0}^n\sum_{k=0}^j\frac{(-1)^k}{k!}\frac{(j-k)^i}{(j-k)!}=\sum_{j=0}^n2^jj!\sum_{k=0}^j\frac{(-1)^k}{k!}\frac{\sum_{i=0}^n(j-k)^i}{(j-k)!}=\sum_{j=0}^n2^jj!\sum_{k=0}^j\frac{(-1)^k}{k!}\frac{(j-k)^{n+1}-1}{(j-k-1)(j-k)!}

最后一步是个等比数列求和。

然后上卷积就行了

2.观察式子发现,需要求的是将i个数划分成j个集合,每个集合贡献2,带标号,总贡献为贡献之积。而n个数的总贡献为积贡献之和。

f(i)=\sum_{j=0}^i\begin{Bmatrix} i\\j \end{Bmatrix}2^jj!

f(i)=\sum_{j=0}^i\binom{i}{j}*2*f(i-j)

即枚举最后一个集合的元素个数,组合数一下,贡献2乘上以前的。

分治FFT即可。然而我还是喜欢第一种。

#include<bits/stdc++.h>
using namespace std;
#define in read()
#define int long long
int in{
	int cnt=0,f=1;char ch=0;
	while(!isdigit(ch)){
		ch=getchar();if(ch=='-')f=-1;
	}
	while(isdigit(ch)){
		cnt=cnt*10+ch-48;
		ch=getchar();
	}return cnt*f;
}
const int mod=998244353;
int n;
int fac[1000003],ifac[1000003];
int ksm(int a,int b){
	int sum=1;
	while(b){
		//cout<<sum<<" "<<a<<endl;
		if(b&1)sum=sum*a%mod;a=a*a%mod;b>>=1;
	}return sum;
}
int a[1000003],b[1000003];
int r[1000003],limit,l;
void getl(int len){
	limit=1;l=0;
	while(limit<len)limit<<=1,l++;
	for(int i=0;i<limit;i++){
		r[i]=(r[i>>1]>>1)|((i&1)<<(l-1));
	}
}int x;int ans=0;
void NTT(int *a,int len,int inv){
	for(int i=0;i<len;i++){
		if(i<r[i])swap(a[i],a[r[i]]);
	}
	for(int mid=1;mid<len;mid<<=1){
		int wn=ksm(3,(mod-1)/(mid<<1));
		for(int i=0;i<len;i+=mid*2){
			int omega=1;
			for(int j=0;j<mid;j++,omega=omega*wn%mod){
				int x=a[i+j],y=a[i+j+mid]*omega%mod;
				a[i+j]=(x+y)%mod;a[i+j+mid]=(x-y+mod)%mod; 
			}
		}
	}if(inv==1)return;
	reverse(a+1,a+len);int gu=ksm(len,mod-2);
	for(int i=0;i<len;i++)a[i]=a[i]*gu%mod;
}int jz[1000003];
int query(int n){
	for(int i=0;i<=n;i++){
		a[i]=(i&1)?mod-1:1;(a[i]*=ifac[i])%=mod;b[i]=((i-1)<0)?(i+mod-1):(i-1);b[i]=ksm(b[i],mod-2)*ifac[i]%mod;
		x=ksm(i,n+1)+mod-1;b[i]=b[i]*x%mod;b[i]=(b[i]+mod)%mod;
	}b[1]=n+1;
//	for(int i=0;i<=n;i++){
//		cout<<a[i]<<" ";
//	}cout<<endl;for(int i=0;i<=n;i++)cout<<b[i]<<" ";cout<<endl;
	getl(n<<1);
	NTT(a,limit,1);NTT(b,limit,1);
	for(int i=0;i<limit;i++)a[i]=a[i]*b[i]%mod;
	NTT(a,limit,-1);
	for(int i=0;i<=n;i++){
		ans=(ans+jz[i]*fac[i]%mod*a[i]%mod)%mod;
	}return ans;
}
signed main(){
	n=in;
	fac[0]=1;jz[0]=1;
	for(int i=1;i<=n;i++)fac[i]=fac[i-1]*i%mod,jz[i]=jz[i-1]*2%mod;
	ifac[n]=ksm(fac[n],mod-2);
	for(int i=n-1;i>=1;i--)ifac[i]=ifac[i+1]*(i+1)%mod;ifac[0]=1;
	//for(int i=0;i<=n;i++)cout<<ifac[i]<<" ";cout<<endl;
	cout<<query(n);
	return 0;
}

这里仍然有一个小小的例题 HDU4625

求所有点到一个点距离的k次方和。k500,n50000。对每个点执行。

直接算会炸,因为从一个m次方推导到另一个m次方需要展开二项式爆算。

但如果另辟蹊径走下降幂维护,最后转回去,会好很多。

首先证明一个结论(x+1)^{\underline k}=(x+1)x^{\underline{k-1}}=(x-k+1)x^{\underline {k-1}}+kx^{\underline {k-1}}=x^{\underline k}+kx^{\underline {k-1}}

这也就是说,维护下降幂答案的复杂度本身是O(1)的。而维护k个下降幂无非就是k次,总复杂度=O(nk);

子树答案可以递归,但父亲答案计算需要容斥。我们详细说明一下。

假设我们已经计算好了到父亲的答案。那么所有从不属于u子树过来的dis到u都是到父亲dis+1。

根据公式为d[fa][k]+k*d[fa][k-1]

可是不幸的是,本来应该直接到u的dis,先去了fa,又回到了u。所以我们实质上多算了一个(alldis[u]+2)^{\underline k}又少算了一个(alldis[u])^{\underline k}

把这个多算的根据公式展开就是

(alldis[u]+2)^{\underline k}-(alldis[u])^{\underline k}=(alldis[u]+1)^{\underline k}+k(alldis[u]+1)^{\underline {k-1}}-(alldis[u])^{\underline k}=(alldis[u])^{\underline k}+k(alldis[u])^{\underline {k-1}}+k((alldis[u])^{\underline{k-1}}+(k-1)(alldis[u])^{\underline {k-2}})-(alldis[u])^{\udnerline k}=2k(alldis[u])^{\underline {k-1}}+k(k-1)(alldis[u])^{\underline {k-2}}

那么我们dfs两次。一次做子树,另一次加上父亲答案并根据上式容斥,最后下降幂二类斯特林转幂就行。

顺带一提,容斥一阶下降幂和零阶下降幂的公式自己去想。(其实就是把不存在的项甩掉罢了)

#include<bits/stdc++.h>
using namespace std;
#define in read()
int in{
	int cnt=0,f=1;char ch=0;
	while(!isdigit(ch)){
		ch=getchar();if(ch=='-')f=-1;
	}
	while(isdigit(ch)){
		cnt=cnt*10+ch-48;
		ch=getchar();
	}return cnt*f;
}
int first[50003],nxt[100003],to[100003],tot;
void add(int a,int b){
	nxt[++tot]=first[a];first[a]=tot;to[tot]=b;
}
int f[50003][503];
int t,n,k;
int ans;
const int mod=1e4+7;
int s[503][503];
void dfs1(int u,int faa){
	f[u][0]=1;
	for(int i=1;i<=k;i++)f[u][i]=0;
	for(int i=first[u];i;i=nxt[i]){
		int v=to[i];if(v==faa)continue;
		dfs1(v,u);f[u][0]=(f[u][0]+f[v][0])%mod; 
		for(int j=1;j<=k;j++){
			f[u][j]=(f[u][j]+f[v][j]+j*f[v][j-1]%mod)%mod;
		}
	} 
}
void dfs2(int u,int fa){
	if(fa){
		for(int j=k;j>=2;j--){
			f[u][j]=(f[fa][j]+f[fa][j-1]*j%mod-2*j*f[u][j-1]%mod-j*(j-1)%mod*f[u][j-2]%mod);(f[u][j]+=10*mod)%=mod;
		}
		f[u][1]=f[fa][1]+f[fa][0]-2*f[u][0];(f[u][1]+=mod*10)%=mod;
		f[u][0]=f[fa][0];
	}
	for(int i=first[u];i;i=nxt[i]){
		int v=to[i];if(v!=fa)dfs2(v,u);
	}
}
int main(){
	t=in;
	s[0][0]=1;
	for(int i=1;i<=500;i++){
		for(int j=1;j<=i;j++){
			s[i][j]=(s[i-1][j-1]+s[i-1][j]*j%mod)%mod;
		}
	}//cout<<"#";
		
	while(t--){
		n=in;k=in;ans=0;
		memset(first,0,sizeof(first));tot=0;
		for(int i=1;i<n;i++){
			int u=in;int v=in;add(u,v);add(v,u);
		}
		dfs1(1,0);
		//for(int i=1;i<=n;i++)cout<<f[i][2]<<endl;
		dfs2(1,0);
//		for(int i=1;i<=n;i++){
//			for(int j=0;j<=k;j++)cout<<f[i][j]<<" ";cout<<endl;
//		}
		for(int i=1;i<=n;i++){ans=0;
			for(int j=0;j<=k;j++){
				ans=(ans+s[k][j]*f[i][j]%mod)%mod;
			}printf("%d\n",ans);
		}
	}
	return 0;
}

今儿就写到这里,,好日龙啊这东西

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值