组合数学相关

时间:2021-08-01
本文章向大家介绍组合数学相关,主要包括组合数学相关使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

1. Lucas & exLucas

1.1 Lucas 定理

先摆结论:\(\dbinom{n}{m}\bmod p=\dbinom{\lfloor n/p\rfloor}{\lfloor m/p\rfloor}\dbinom{n\bmod p}{m\bmod p}\bmod p\)

证明:考虑组合数的定义,因为 \(\forall i\in[1,p-1],\dbinom{p}{i}\equiv 0\pmod p\),且 \(\dbinom{p}{0}=\dbinom{p}{p}=1\),所以 \((a+b)^p\equiv a^p+b^p\pmod p\)

因此,\((1+x)^n=(1+x)^{p\lfloor n/p\rfloor}(1+x)^{n\bmod p}\equiv (1+x^p)^{\lfloor n/p\rfloor}(1+x)^{n\bmod p}\pmod p\)

因为我们只关心 \(x^m\) 前的系数,而 \((1+x^p)^{\lfloor n/p\rfloor}\) 得到的所有指数都为 \(p\) 的倍数,\((1+x)^{n\bmod p}\pmod p\) 得到的所有指数都小于 \(p\),所以此时 \(m\) 只能被写成 \(p\lfloor m/p \rfloor+m\bmod p\),得证。

预处理阶乘及其逆元 \(\mathcal{O}(1)\) 求组合数,则总时间复杂度为 \(\mathcal{O}(p+log_{p}n)\)

1.2. 例题

I. P3807 【模板】卢卡斯定理

#include <bits/stdc++.h>
using namespace std;

#define ll long long

const int N=1e5+5;
ll ksm(ll a,ll b,ll p){
	ll s=1;
	while(b){
		if(b&1)s=s*a%p;
		a=a*a%p,b<<=1;
	} return s;
}

ll fc[N<<1],ifc[N];
ll c(ll n,ll m,ll p){
	if(n<m)return 0;
	if(n<p)return fc[n]*ifc[m]%p*ifc[n-m]%p;
	return c(n/p,m/p,p)*c(n%p,m%p,p)%p;
}

int main(){
	int t,n,m,p;
	cin>>t;
	while(t--){
		cin>>n>>m>>p;
		fc[0]=1;
		for(int i=1;i<p;i++)fc[i]=fc[i-1]*i%p;
		ifc[p-1]=ksm(fc[p-1],p-2,p);
		for(int i=p-2;~i;i--)ifc[i]=ifc[i+1]*(i+1)%p;
		cout<<c(n+m,n,p)<<endl;
	}
	return 0;
}

2. Prufer 序列

感觉很多时候生成树计数可以用 Prufer 序列来计算,故学习该算法。

2.1. 树生成 Prufer 序列

Prufer 序列的定义是这样的:给定一棵树 \(T\),每次找到编号最小的叶子结点 \(a\) 并将与其相邻的 \(b\) 加入 Prufer 序列中,删除 \(a\)。重复操作直至树中只剩下 \(2\) 个节点。显然,Prufer 序列的长度为 \(n-2\)

具体地,用指针维护 \(a\)。删除 \(a\) 之后:

\(b<a\)\(b\) 变为叶子结点,那么删除 \(b\),将与其相邻的 \(b’\) 加入序列。操作递归进行,即若 \(b'<b\)\(b'\) 变为叶子结点,那么删除 \(b'\),将与其相邻的 \(b’’\) 加入序列;若 \(b''<b\)\(b''\) 变为叶子结点,那么删除 \(b''\),将与其相邻的 \(b'''\) 加入序列,以此类推。

最后 \(a\) 自增找到下一个编号最小的叶子结点。

时间复杂度为 \(n\)

2.2. Prufer 序列生成树

设点集 \(a\) 包含 \(1\sim n\) 所有的点,每次取出 Prufer 序列 \(b\) 第一个数 \(u\),在点集 \(a\) 中找到最小的没有在 \(b\) 中出现过的数 \(v\),连边 \((u,v)\) 并将 \(u,v\) 分别在序列 \(b,a\) 中删除。重复操作直至 \(b\) 中没有元素。显然,\(a\) 中还剩下两个元素 \(a_1,a_2\),则连边 \((a_1,a_2)\)

具体地,用指针维护 \(v\)。连边 \((u,v)\) 之后:

\(u<v\)\(u\)\(b\) 中是最后一次出现,那么将 \(u\)\(b\) 中下一个数 \(u’\) 相连。操作递归进行,即若 \(u’<u\)\(u’\)\(b\) 中是最后一次出现,那么将 \(u’\)\(b\) 中下一个数 \(u''\) 连边;若 \(u''<u'\)\(u''\)\(b\) 中是最后一次出现,那么将 \(u’’\)\(b\) 中下一个数 \(u'''\) 相连,以此类推。

最后 \(v\) 自增找到下一个最小的没有在 \(b\) 中出现过的数。

时间复杂度为 \(n\)

2.3. Prufer 序列得到的一些推论

  1. \(n\) 个节点的有标号无根树个数为 \(n^{n-2}\)。这也是 \(n\) 个节点的无向完全图的生成树个数。
  2. \(n\) 个节点的有标号有根数个数为 \(n^{n-1}\)。对于每个无根树,钦定任意一个节点为根都可以形成唯一的有根树,因此将 \(n^{n-2}\) 乘上 \(n\) 即可。
  3. 度数为 \(d\) 的节点在 Prufer 序列中出现了 \(d-1\) 次,这个由 Prufer 序列的构造方式可知。
  4. \(n\) 个有度数要求 \(d_i\) 的节点的有标号无根树个数为 \(\dfrac{(n-2)!}{\prod_{i=1}^n(d_i-1)!}\),这个由推论 3 和多重集的排列数可知。

2.4. 例题

*I. CF156D Clues

HOT TEA.

如果将同一个连通块缩成点,设最终剩下 \(k\) 个点,那么它的 Prufer 序列长度为 \(k-2\),且每个位置都可以填 \(1\sim n\) 之间的任意数,因此方案数为 \(n^{k-2}\)

但是,从节点集合 \(a\)(原来包含 \(1\sim k\),对应连通块的编号)中取出最小的不包含 \(b\) 中出现节点的连通块编号 \(v\) 时,我们并不知道选择的是连通块内的哪一个节点,因此要乘上对应的连通块大小 \(s_v\)。因此答案为 \(n^{k-2}\times \prod_{i=1}^k s_i\)。注意 \(k=2\) 时需要特判输出 \(1\bmod p\)

时间复杂度 \(n\log n\)。直接 DFS 可以做到 \(n\)

#include <bits/stdc++.h>
using namespace std;

#define ll long long
#define gc getchar()

inline int read(){
	int x=0,sign=0; char s=gc;
	while(!isdigit(s))sign|=s=='-',s=gc;
	while(isdigit(s))x=(x<<1)+(x<<3)+(s-'0'),s=gc;
	return sign?-x:x;
}

const int N=1e5+5;

ll n,m,k,p,f[N],sz[N],ans=1;
int find(int x){return f[x]==x?x:f[x]=find(f[x]);}
void merge(int x,int y){
	x=find(x),y=find(y);
	if(x==y)return;
	if(sz[x]<sz[y])swap(x,y);
	sz[x]+=sz[y],f[y]=x;
}

int main(){
	cin>>n>>m>>p;
	for(int i=1;i<=n;i++)f[i]=i,sz[i]=1;
	for(int i=1,x,y;i<=m;i++)merge(x=read(),y=read());
	for(int i=1;i<=n;i++)if(f[i]==i)ans=ans*sz[i]%p,k++;
	if(k==1)cout<<1%p<<endl;
	else{
		while((k--)-2)ans=ans*n%p;
		cout<<ans<<endl;
	}
	return 0;
}

原文地址:https://www.cnblogs.com/alex-wei/p/Combinatorial_Mathematics.html