首页 > 其他分享 >组合数学初步

组合数学初步

时间:2024-11-07 15:42:49浏览次数:1  
标签:frac 组合 int res return times 初步 数学 pk

组合数学初步

基本计数原理

加法原理:若完成一个事件 \(A\) 有 \(n\) 类方法,第 \(i\) 类有 \(s_i\) 种不同的方案,则完成事件 \(A\) 有 \(\sum^{n}_{i=1} s_i\) 种方案。

乘法原理:若完成一个事件 \(A\) 需要 \(n\) 步,第 \(i\) 步有 \(k_i\) 种不同的方式,则完成事件 \(A\) 有 \(\prod_{i=1}^{n}k_i\) 种不同的方案。

减法(简单容斥)原理:若完成事件 \(A\) 共有 \(n\) 次尝试,其中 \(c\) 次不能完成,则其中能完成的次数为 \(n-c\)。

排列

从 \(n\) 个人中选 \(m\) 个人排成一排,问方案数。

解:

第一个人有 \(n\) 种选择,第二个人有 \(n-1\) 种选择……第 \(m\) 个人有 \(n-m+1\) 种选择。

分步乘法得 \(\prod _{i=1}^m n-i+1=\frac{n!}{(n-m)!}\),记为 \(A_n^m\)。

组合

从 \(n\) 个人中选 \(m\) 个人,问方案数。

解:无需排成一排,把重复的去掉即可。

答案为 \(\frac{n!}{m!(n-m)!}\),记为 \(C_n^m\) 或者 \(\tbinom{n}{m}\)。

性质:

  • \(C_n^m=C_n^{n-m}\)。

  • \(\sum_{i=0}^n=2^n\)。

  • \(\forall k>0,C_n^k=C_{n-1}^k+C_{n-1}^{k-1}\)。

  • \(C_{n}^m=C_{n-1}^{m-1}\times \frac{n}{m}\)。

  • \(\sum_{i=1}^n C_{i+m}^i=C_{m+n+1}^{m+1}-1\)

证明:

\[\begin{aligned} &\hspace{0.5cm}\sum_{i=1}^n C_{i+m}^i\\ &=\sum_{i=1}^n C_{i+m}^m\\ &=\sum_{i=1}^n C_{i+m}^m+C_{i+1}^{i+1}-1\\ &=C_{m+n+1}^{m+1}-1 \end{aligned} \]

组合数的一般求法

  • 暴力乘

时间复杂度 \(O(n)\)。

1.\(T\) 组数据,每次求 \(C_n^m \bmod p\),\(p\) 固定,\(m,n\le 10^3\)。

  • 杨辉三角递推

根据归纳恒等式:

\[\forall k>0,C_n^k=C_{n-1}^k+C_{n-1}^{k-1} \]

可以递推,预处理 \(O(n^2)\),单次 \(O(1)\)。

int C[N][N];
C[0][0]=1;
FOR(i,1,n) {
	C[i][0]=1;
	FOR(j,1,n) C[i][j]=C[i][j-1]+C[i-1][j-1];
}

2.\(T\) 组数据,每次求 \(C_n^m \bmod p\),\(p\) 固定,\(m,n\le 10^5\)。

  • 预处理阶乘

在取模时需要求逆元。

预处理 \(O(n)\),单次复杂度 \(O(1)\)。

int C(int n,int m) {
	if(n<m) return 0;
	return fac[n]*inv[m]%Mo*fac[n-m]%Mo;
}
fac[0]=1;
FOR(i,1,n) fac[i]=fac[i-1]*i%Mo;
inv[n]=power(fac[i],Mo-2);
ROF(i,n,1) inv[i-1]=inv[i]*i%Mo;

3.求 \(C_n^m \bmod p\),\(n,m\le 10^{18},p\le 10^5,p\in \mathbb{P}\)。

  • 卢卡斯定理

若 \(p\) 为质数,则有:

\[C_n^m=C_{\lfloor n/p \rfloor}^{\lfloor m/p \rfloor}\times C_{n\bmod p}^{m\bmod p} \]

证明:

引理:\(p\in \mathbb{P},x^p+1\equiv (x+1)^p\pmod p\)

证明:后者用二项式定理展开:

\[(x+1)^p=\sum_{i=0}^p C_p^i x^i=(\sum_{i=1}^{p-1} C_p^i x^i) + 1 + x^p \]

显然,括号内均为 \(p\) 的倍数,可消掉。

证毕。

设 \(n=ps+c,m=pt+d\)

故原命题等价于求证 \(C_n^m=C_s^t\times C_c^d\)。

\(C_n^m\) 在 \((1+x)^n\) 中等价于 \(x^m\) 的系数。

在 \(\bmod p\) 意义下,\((1+x)^p\equiv 1+x^p \pmod p\)。

根据二项式定理,有:

\[\begin{aligned} &\hspace{0.6cm} (1+x)^n\\ &=(1+x)^{ps+c}\\ &=(1+x)^{ps}\times (1+x)^c\\ &=(\sum_{i=0}^s C_s^i x^{ip})\times (\sum_{i=0}^c C_c^i x^i) \end{aligned} \]

又 \(x^m=x^{pt+d}=x^{pt}\times x^d\)。

因为 \(s<p\),所以后面不可能提供 \(x^{pt}\),前面也不可能提供 \(x^{d}\)。

故 \(x^m\) 一定是两式相乘而得。

故原命题成立。

证毕。

const int N=1e5+10;
int t,fac[N];
LL power(LL a,LL b,LL p) {
	LL res=1;
	for(;b;b>>=1) {
		if(b&1) res=(res*a)%p;
		a=a*a%p;
	}
	return res%p;
}
LL C(int n,int m,int p) {
	if(n<m) return 0;
	return fac[n]*power(fac[m],p-2,p)%p*power(fac[n-m],p-2,p)%p;
}
LL lucas(int n,int m,int p) {
	if(n<p&&m<p) return C(n,m,p);
	return C(n%p,m%p,p)*lucas(n/p,m/p,p)%p;
}
int n,m,Mo;
void solve() {
	cin>>n>>m>>Mo;n=n+m;
	FOR(i,1,Mo) fac[i]=fac[i-1]*i%Mo;
	cout<<lucas(n,m,Mo)<<"\n";
}
main() {
	cin>>t;fac[0]=1;
	while(t--) solve();
	return 0;
}

预处理 \(O(p)\),单次 \(O(\log n)\)。

  • 求 \(C_n^m\),\(n,m\le 10^3\),不取模

高精度除法直接秒了!

发现无论除的是什么 妖魔鬼怪,最后总会是一个非负整数。

所以考虑对阶乘分解质因数,将除法算式转化为质数幂的指数相减的形式。

最后只需要做高精度乘法了。

单次复杂度 \(O(n)\)。

(本代码以洛谷P1771为例)

const int N=1e5+10;
int n,x,v[N],phi[N];
vector<int>prime;
LL power(LL a,LL b,LL p) {
	LL res=1%p;
	for(;b;b>>=1) {
		if(b&1) res=(res*a)%p;
		a=(a*a)%p;
	}
	return res;
}
void Euler() {
	phi[1]=1;
	FOR(i,2,100000) {
		if(!v[i]) phi[i]=i-1,v[i]=i,prime.pb(i);
		for(int p:prime) {
			if(i*p>100000||v[i]<p) break;
			v[i*p]=p;
			phi[i*p]=phi[i]*(i%p?p-1:p); 
		}
	}
}
int sum[N];
int Get(int x,int p) {
	int cnt=0;
	while(x) {
		cnt+=x/p;
		x/=p;
	}
	return cnt;
}
vector<int>Mul(vector<int>x,int y) {
	LL tol=0;vector<int>res;
	for(auto c:x) {
		tol+=(LL)y*c;
		res.pb(tol%10);
		tol/=10;
	}
	while(tol) res.pb(tol%10),tol/=10;
	return res;
}
main() {
	cin>>n>>x;
	x=power(x,x,1000);
	Euler();
	for(auto p:prime) sum[p]=Get(x-1,p)-Get(n-1,p)-Get(x-n,p);
	vector<int>ans;ans.pb(1);
	for(auto p:prime) while(sum[p]--) ans=Mul(ans,p);
	reverse(ans.begin(),ans.end());
	for(auto x:ans) write(x);putchar('\n');
	return 0;
}
  • 求 \(C_n^m \bmod p\) 的值,其中 \(n,m\le 10^9\),\(p\) 不一定为质数,但保证 \(p\) 的所有质因子幂次均为 \(1\)。

将 \(p\) 分解质因数,分别求出原式对 \(p\) 的各个质因子取模的结果。

最后用 中国剩余定理 解同余方程组即可。

(这里以洛谷 P2840 为例)

const int Mo=999911659,N=4e4+10;
int p[4]={2,3,4679,35617},M[4],t[4],a[4],inv[N],fac[N];
int n,g;
int power(int x,int y,int p) {
	int res=1;
	for(;y;y>>=1) {
		if(y&1) res=res*x%p;
		x=x*x%p;
	}
	return res;
}
int C(int n,int m,int p) {
	if(n<m) return 0;
	return fac[n]*inv[m]%p*inv[n-m]%p;
}
int Lucas(int n,int m,int p) {
	if(n<p&&m<p) return C(n,m,p);
	return C(n%p,m%p,p)*Lucas(n/p,m/p,p)%p;
}
vector<int>c; 
void Init(int id) {
	int P=p[id];
	fac[0]=1;
	FOR(i,1,P-1) fac[i]=fac[i-1]*i%P;
	inv[P-1]=power(fac[P-1],P-2,P);
	ROF(i,P-1,1) inv[i-1]=inv[i]*i%P;
	for(int x:c) (a[id]+=Lucas(n,x,P))%=P;
} 
int Crt() {
	int m=Mo-1;
	FOR(i,0,3) M[i]=m/p[i],t[i]=power(M[i],p[i]-2,p[i]);
	int ans=0;
	FOR(i,0,3) (ans+=a[i]*M[i]%m*t[i]%m)%=m;
	return ans;
}
int solve() {
	for(int i=1;i*i<=n;++i) {
		if(n%i) continue;
		c.pb(i);
		if(i==n/i) continue;
		c.pb(n/i);
	}
	FOR(i,0,3) Init(i);
	return Crt();
}
main() {
	cin>>n>>g;
	if(g==Mo) {
		puts("0");
		return 0;
	}
	cout<<power(g,solve(),Mo)<<"\n";
	return 0;
}
  • 求 \(C_n^m \bmod p\) 的值,其中 \(p\) 不一定为质数,\(n,m\le 10^5\)。

跟高精度的思想差不多,乘的时候顺带取模就行了,代码不放了。

  • 求 \(C_n^m \bmod q\) 的值,其中 \(n,m\le 10^{18},q\le 10^6\)

洛谷p4720 扩展lucas定理

这个刺激了

类似上述思想,将 \(q\) 分解质因数,设 \(q\) 的唯一分解为 \(p_1^{c_1}p_2^{c_2}p_3^{c_3}\dots p_o^{c_o}\)。

求出同余方程组

\[\begin{cases} x\equiv C_n^m \pmod {p_1^{c_1}}\\ x\equiv C_n^m \pmod {p_2^{c_2}}\\ x\equiv C_n^m \pmod {p_3^{c_3}}\\ \dots\\ x\equiv C_n^m \pmod {p_o^{c_o}}\\ \end{cases} \]

问题转换成如何求出 \(C_n^m \bmod p^c\) 的值。

这个式子等价于:

\[\frac{n!}{m!\cdot (n-m)!}\bmod p^c \]

这玩意没办法用逆元,考虑转化。

由于 \(p^c\) 仅有一个本质不同的质因子 \(p\),考虑在 \(m!\) 与 \((n-m)!\) 除掉 \(p\) 的若干次幂,使得分母与模数互质,这样就可以用扩展欧几里得求逆元了。

但是,剩下被除掉的部分还是不和模数互质,故还需要在分子上除掉若干次幂消掉分子的被除掉的贡献。

这样式子就变成了

\[\frac{\frac{n!}{p^x}}{\frac{m!}{p^y}\frac{(n-m)!}{p^z}}\times p^{x-y-z} \bmod p^c \]

问题转换成求 \(\frac{n!}{p^x}\)。

\[\begin{aligned} &\hspace{0.6cm} n!\\ &=n\times (n-1)\times (n-2)\times \dots \times 1\\ &=(p\times 2p\times 3p\dots)\times (1\times 2\times 3\dots)\\ &=p^{\lfloor\frac{n}{p}\rfloor}\times \lfloor\frac{n}{p}\rfloor!\times (1\times 2\times 3\dots) \end{aligned} \]

注意到上面的 \(p^{\lfloor\frac{n}{p}\rfloor}\) 最后一定要被除掉,并发现上述 \(\lfloor\frac{n}{p}\rfloor!\) 还可能有 \(p\) 因子存在,故设递推式:

\[f(n)=f(\lfloor\frac{n}{p}\rfloor)\times (1\times 2\times 3\dots) \]

这样求解目标就是 \(f(n)\),递归边界为 \(f(0)=1\)。

问题转换成求 \(f(n)\) 后面带的那一串乘法算式。

注意到后面的所有乘数都不是 \(p\) 的倍数,所以 \(l<p^c,l,p^c+l,2p^c+l\dots (\lfloor\frac{n}{p^c}\rfloor-1)\times p^c+l\) 其实在 \(\bmod p^c\) 意义下是等价的,都是 \(l\)。

所以只求小于 \(p^c\) 部分的乘积,中间部分直接对小于 \(p^c\) 的部分做快速幂,剩下的 \(\lfloor\frac{n}{p^c}\rfloor\times p^c \sim n\) 的部分暴力乘即可。

求一次 \(f(n)\) 复杂度是 \(O(p^c \log n)\) 的,这个 \(\log\) 是以 \(p\) 为底的,所以比一般的 \(\log\) 要小。

int f(int n,int p,int pk) {
	if(!n) return 1;//边界
	int st=1,ed=1;
	FOR(i,1,pk) if(i%p) (st*=i)%=pk;//暴力求开始部分
	st=power(st,n/pk,pk);//快速幂求中间贡献
	FOR(i,pk*(n/pk),n) if(i%p) (ed*=(i%pk))%=pk;//余项
	return f(n/p,p,pk)*st%pk*ed%pk;//递归求解n/p
}

问题来了,怎么求 \(p^{x-y-z}\)?

其实就是求 \(n!\) 中有多少 \(p\) 这个质因子。

事实上,在 \(f(n)\) 的过程中,已经顺便求出来了。

\(p^{\lfloor\frac{n}{p}\rfloor}\times \lfloor\frac{n}{p}\rfloor!\times (1\times 2\times 3\dots)\)

的这个 \(p^{\lfloor\frac{n}{p}\rfloor}\),将递归过程中的 \(\lfloor\frac{n}{p}\rfloor\) 加起来就行了。

int g(int x,int p) {//求 x! 中有多少 p
	int cnt=0;
	while(x) {
		cnt+=x/p;
		x/=p;
	}
	return cnt;
} 

求出 \(f(n)\) 本身与 \(f(m),f(n-m)\) 在 \(\bmod p^c\) 意义下的逆元,乘起来后对 \(p^c\) 取模就是 \(C_n^m \bmod p^c\)。

int inv(int a,int p) {
	int x,y;
	exgcd(a,p,x,y);
	return (x+p)%p;
}
int C(int n2,int m2,int p,int pk) {
	int nc=f(n2,p,pk),mc=inv(f(m2,p,pk),pk),nmc=inv(f(n2-m2,p,pk),pk);//n!,1/m!,1/(n-m)!
	int pa=power(p,g(n2,p)-g(m2,p)-g(n2-m2,p),pk);//p^(x-y-z)
	return nc*mc%pk*nmc%pk*pa%pk;//乘积。
}

最后,使用中国剩余定理合并方程组,即可求出答案,总时间复杂度 \(O(q \log n)\)。

int exLucas() {
	int nw=p;
	for(int i=2;i*i<=p;++i) {
		if(nw%i) continue;
		int cy=0; 
		while(nw%i==0) ++cy,nw/=i;
		vec.pb(mkp(i,cy));
	}
	if(nw>1) vec.pb(mkp(nw,1));
	for(auto o:vec) {
		int x=o.fr,y=o.se,pk=1;
		FOR(i,1,y) pk*=x;
		crt.pb(mkp(C(n,m,x,pk),pk)); 
	}
	int m=1,ans=0;
	for(auto o:crt) m*=o.se;
	for(auto o:crt) {
		int x=o.se,M=m/x;
		int l,r;exgcd(M,x,l,r);
		l=(l+m)%m;
		(ans+=l%m*M%m*o.fr%m)%=m;
	}
	return ans;
}

全部代码如下:

const int N=1e6+10;
int n,m,p,M[N];
void exgcd(int a,int b,int &x,int &y) {
	if(!b) return x=1,y=0,void();
	exgcd(b,a%b,x,y);
	int t=x;x=y;y=t-(a/b)*y;
}
int mul(int a,int b,int p) {
	int res=0;a%=p,b%=p;
	for(;b;b>>=1) {
		if(b&1) res=(res+a)%p;
		a=a*2LL%p;
	}
	return res;
}
int power(int a,int b,int p) {
	int res=1%p;a%=p;
	for(;b;b>>=1) {
		if(b&1) res=mul(res,a,p)%p;
		a=mul(a,a,p)%p;
	}
	return res;
}
int f(int n,int p,int pk) {
	if(!n) return 1;
	int st=1,ed=1;
	FOR(i,1,pk) if(i%p) (st*=i)%=pk;
	st=power(st,n/pk,pk);
	FOR(i,pk*(n/pk),n) if(i%p) (ed*=(i%pk))%=pk;
	return f(n/p,p,pk)*st%pk*ed%pk;
}
int g(int x,int p) {
	int cnt=0;
	while(x) {
		cnt+=x/p;
		x/=p;
	}
	return cnt;
} 
int inv(int a,int p) {
	int x,y;
	exgcd(a,p,x,y);
	return (x+p)%p;
}
int C(int n2,int m2,int p,int pk) {
	int nc=f(n2,p,pk),mc=inv(f(m2,p,pk),pk),nmc=inv(f(n2-m2,p,pk),pk);
	int pa=power(p,g(n2,p)-g(m2,p)-g(n2-m2,p),pk);
	return nc*mc%pk*nmc%pk*pa%pk;
}
vector<pair<int,int> >vec,crt;
int exLucas() {
	int nw=p;
	for(int i=2;i*i<=p;++i) {
		if(nw%i) continue;
		int cy=0; 
		while(nw%i==0) ++cy,nw/=i;
		vec.pb(mkp(i,cy));
	}
	if(nw>1) vec.pb(mkp(nw,1));
	for(auto o:vec) {
		int x=o.fr,y=o.se,pk=1;
		FOR(i,1,y) pk*=x;
		crt.pb(mkp(C(n,m,x,pk),pk)); 
	}
	int m=1,ans=0;
	for(auto o:crt) m*=o.se;
	for(auto o:crt) {
		int x=o.se,M=m/x;
		int l,r;exgcd(M,x,l,r);
		l=(l+m)%m;
		(ans+=l%m*M%m*o.fr%m)%=m;
	}
	return ans;
}
main() {
	cin>>n>>m>>p;
	cout<<exLucas()<<"\n";
	return 0;
}

讲个笑话,扩展 lucas 和 lucas 没有任何关系。

组合数经典问题

例1.[HNOI2008] 越狱

直接求很不好做。

考虑上面的减法原理,用总数减去不会越狱的方案数。

总数显然是 \(m^n\)。

而不会越狱当且仅当相邻两个监狱的信仰不同。

所以第一个可以有 \(m\) 种选择,第二个可有 \(m-1\) 种选择(不能选第一个人选的),第三个有 \(m-1\) 种选择(不能选第二个人的)……

所以答案为 \(m^n-m\times (m-1)^{n-1}\)。需要注意减法取模的情况。

例2.错排问题

设 \(f(n)\) 表示当排列长度为 \(n\) 时该问题答案。

考虑递推,假设 \(n\) 放在了第 \(k\) 个位置,则有 \(n-1\) 种选择。

则此时第 \(k\) 个信封有两种选择:

  • 放在第 \(n\) 个位置,则剩余 \(n-2\) 个再次错排,有 \(f(n-2)\) 种方案。

  • 不放在第 \(n\) 个位置,则剩余 \(n-1\) 个错排,有 \(f(n-1)\) 种方案。

上述先放 \(n\),再放 \(k\),分两步,用乘法原理。第二步分两类,用加法原理。

则 \(f(n)=(n-1)\times (f(n-1)+f(n-2))\)。

例3.[SDOI2016] 排列计数

先选几个固定后,剩下的错排即可。

例4.[NOIP2016 提高组] 组合数问题

\(n,m\) 很小,使得我们可以杨辉三角递推组合数。

注意到 \(k\) 固定,所以递推时可以对 \(k\) 取模,一个组合数能够整除 \(k\) 当且仅当该数字 \(\bmod k\) 后为 \(0\)。

最后做二维前缀和即可做到每次 \(O(1)\) 查询。

例5.方程的解

给你一个不定方程 \(\sum _{i=1}^k x_i =g(n)\),求正整数解个数。

首先,\(g(n)\) 可以通过快速幂得到。

考虑这样一个问题:

给你 \(n\) 个相同的球与 \(k\) 个不同的盒子,每个盒子至少放一个,求将这 \(n\) 个球全部放入 \(k\) 个盒子的方案数。

将这 \(n\) 个球排成一排,最左侧和最右侧分别放上一个板子,如下:

\[|\operatorname{ooooooooooooooo}| \]

这样问题转换成放入 \(k-1\) 个板子(不算两边的板子),两个板子之间至少有一个球,求方案数。

如下即为一个 \(k=3\) 的合法方案:

\[|\operatorname{ooo|ooooooo|ooooo}| \]

最终每个盒子放入的球就是两个板子之间的球的数量。

注意到球之间共有 \(n-1\) 个空隙,我们只要在选择 \(k-1\) 个空隙,对每个空隙分别插入一个板子就一定能够构造出一个合法方案。

问题转换成从 \(n-1\) 个东西中选出 \(k-1\) 个东西,求方案数。

答案很显然了,\(C_{n-1}^{k-1}\)!

回到上面的问题,我们抽象一下,将 \(g(x)\) 个 \(1\) 排成一排,有 \(k\) 个数,初始为 \(0\),每个数要选择一些 \(1\) 加到自己上面,不能不选,求方案数。

这不就是放球问题吗?

这样问题就完美解决,答案即为 \(C_{g(x)-1}^{k-1}\)。

不过,该题没有模数,需要写高精。

扩展:给定不定方程 \(\sum_{i=1}^{k} x_i=n\),求非负整数解的个数。

直接求显然不好做。

考虑做一些数学变换。

\[\begin{aligned} &\hspace{0.6cm}\sum_{i=1}^k x_i=n\\ &\Rightarrow \sum_{i=1}^k (x_i+1)=n+k\\ &\Rightarrow \sum_{i=1}^k y_i=n+k \end{aligned} \]

上面的 \(y_i\) 是换元的 \(x_i+1\)。

发现 \(y_i\) 的取值与 \(x_i\) 一一对应。

所以我们求出 \(y_i\) 的解数就可以求出 \(x_i\) 的解数。

而因为 \(y_i\ge 1\),所以这题做完了。

\[C_{n+k-1}^{k-1} \]

例6.[USACO09FEB] Bulls And Cows S

枚举放多少头公牛,假设放 \(p\) 头。

则母牛剩下 \(n-p\) 头。

问题变成放 \(p\) 个板子隔成,左、右留的位置无限制,但板子之间必须空 \(k\) 个以上。

写成不定方程的形式就是

\[\sum_{i=1}^{p+1} x_i=n-p,x_1,x_{p+1}\ge 0,\forall i\in[2,p]\cap \mathbb{Z},x_i\ge k \]

考虑像换元一样,将左边除左右两边的所有项减去 \(k\),原式可化为:

\[\sum_{i=1}^{p+1} x_i=n-p-k\times (p-1),\forall i\in[1,p+1]\cap \mathbb{Z},x_i\ge 0 \]

这样就是不定方程非负整数解板子了,放置公牛 \(p\) 个的答案即:

\[C_{n-k\times (p-1)+1}^p \]

最终答案即:

\[\sum_{p=0}^n C_{n-k\times (p-1)+1}^p \]

按原式计算即可。

例7.车的放置

先考虑一个 \(n\) 行 \(m\) 列的普通矩形放 \(k\) 个怎么做。

首先肯定是在 \(m\) 行中选 \(k\) 列,方案数为 \(C_m^k\)。

其次,选第一个所占得行时有 \(n\) 种方案,第二个有 \(n-1\) 种。

所以方案数有 \(C_m^k\times A_n^k\) 种。

接着将原图形分成两个矩形,这里设分成 \(a\times b\) 与 \((a+c)\times d\) 的矩形。

先放第一个,枚举放多少个(设为 \(k\) 个),方案有 \(C_a^k\times A_b^k\) 种。

再放第二个,设第二个放了 \(z\) 个,但是放第二个时由于第一个放 \(k\) 个后第二个的行只有 \(a+c-k\) 个了。

所以方案数是 \(C_{a+c-k}^z\times A_d^z\)。

乘起来就是答案。

例8.[CQOI2014] 数三角形

一个 \(N\times M\) 的网格上有 \((N+1)\times (M+1)\) 个格点,为了叙述方便,以下记 \(n=N+1,m=M+1\)。

先不考虑不在同一条直线上的限制,就是在所有点中任选 \(3\) 个,方案显然有 \(C_{nm}^3\) 种。

考虑在此基础上减掉不合法的方案。

显然,不合法方案分成横着、竖着、斜着的。

对于横着,一共 \(n\) 行,每一行都有 \(C_m^3\) 种方案,乘起来即可。

竖着的同理。

斜着的有两种情况:斜率正与斜率负。

显然他们是对称的,即方案数相同。

只需要求出斜率为正的方案,最终乘 \(2\) 即可。

注意到我们可以枚举两个不在同一水平线且不在同一竖直线上的点,这样它们中间的点的个数(不包括两端)就是答案。

因为任选中间的一个点与两端的两个点总是可以在同一水平线上。

引理:在格点上,若一个点坐标为 \((a,b)\),另一个点坐标为 \((c,d)\),则它们连线上格点个数(不包括两端)为

\[\gcd(|a-c|,|b-d|)-1 \]

证明:

为叙述方便,以下把绝对值去掉。

对于斜边上任意一点 \((x,y)\),满足 \(\frac{y-b}{x-a}=\frac{d-b}{c-a},x\in[a,c],y\in[b,d]\)。

来一张图。

记 \(g=\gcd(d-b,c-a)\)。

则:

\[\begin{aligned} &\hspace{-0.23cm}y=\frac{d-b}{c-a}\times (x-a)+b\\ &=\frac{\frac{d-b}{g}}{\frac{c-a}{g}}\times (x-a)+b\\ &=\frac{d-b}{g}\times \frac{x-a}{\frac{c-a}{g}}+b \end{aligned} \]

若 \(y\in \mathbb{N}\),则 \(\frac{c-a}{g}\mid{x-a}\)。

则一定存在一个 \(q\),使得 \(\frac{c-a}{g}\times q=x-a\)。

则 \(x=\frac{c-a}{g}\times q+a\)。

经过一堆不等式变换后(或者按原不等式取值范围)得到 \(\frac{c-a}{g}\times q+a\in[a,c]\)。

最终得到 \(q\in[0,g]\cap\mathbb{Z}\)。

即 \(q\) 最多有 \(g+1\) 种取值,即最多有 \(g+1\) 种合法的 \(x\)。

掐头去尾即可证明原命题。

证毕。

直接枚举斜边的端点,计算即可。

事实上,直接枚举是 \(O(n^2m^2 \log n)\) 的,需要进一步优化。

实际上,我们固定形状后,格子图中有多少个这类形状的三角形已经确定了。

假设底、高分别为 \(a,b\),则数量显然为 \((n-a+1)\times (m-b+1)\)。

证明留给读者思考。

复杂度 \(O(nm \log n)\)

int ans,n,m,s[1010][1010],a[1010][1010];
int gcd(int x,int y) {
	return y?gcd(y,x%y):x;
}
int calc(int x) {
	return x*(x-1)*(x-2)/6;
} 
main() {
	cin>>n>>m;
	LL c=(n+1)*(m+1); 
	ans=calc(c)-(m+1)*calc(n+1)-(n+1)*calc(m+1);
	FOR(i,1,n+1) FOR(j,1,m+1) ans-=2LL*(gcd(i,j)-1)*(n-i+1)*(m-j+1);
	cout<<ans<<"\n";
	return 0;
}

多重集排列数

多重集是一种广义的集合。

普通的集合是不可重的,即集合中每个元素都不相同。

多重集是一种可重集合,可以表示成这么一种形式:

\[A=\{n_1\times a_1,n_2\times a_2,\dots,n_k\times a_k\} \]

上述可重集 \(A\) 就是描述了一个集合内有 \(n_1\) 个 \(a_1\)、\(n_2\) 个 \(a_2\)…… \(n_k\) 个 \(a_k\)。

所谓排列数,就是求 \(A\) 有多少个本质不同的排列。

记 \(n=\sum_{i=1}^k n_i\)。

则上述问题的答案为:

\[\frac{n!}{\prod_{i=1}^k n_i!} \]

简证:

不考虑数字相同的情况,共有 \(n!\) 种。

每一个相同的数字内部总共会有 \(n_i!\) 种,需要除掉。

证毕。

例9.Counting swap

将 \(i\) 与 \(p_i\) 之间连有向边,容易发现一定会形成若干个有向环。

若交换完成,则一定是 \(n\) 个自环。

而最优解一定是每次交换都能够让至少一个数字复位,在图上的表现就是让一个大环断成两个小环。

设 \(f_i\) 表示让一个长为 \(i\) 的大环分解成 \(i\) 个自环的方案数。

再设 \(g(x,y)\) 表示将一个长为 \(x+y\) 的环分成长为 \(x\) 和长为 \(y\) 的环的方案数。

注意到若 \(x=y\),\(g(x,y)=x\),否则 \(g(x,y)=x+y\)。

因为第一种可能会有重复情况,画图即可理解,这里不再赘述。

有转移:

\[f_i=\sum_{x=1}^{\lfloor \frac{i}{2} \rfloor} f_x\times f_y\times g(x,y) \]

这样就完了。……吗?

不然,考虑上述乘法原理,我们先把 \(x\) 环消掉再做 \(y\) 环,显然漏了情况。

其实可以先将 \(x\) 环分成两个,再将 \(y\) 环分成两个……

交替着做也是一种方案!肿么办?

发现一个长为 \(x\) 的环操作次数必然是 \(x-1\)。

则上面的操作总数一共是 \(x+y-2\),要从中选 \(x-1\) 次操作给第一个环,剩下的给第二个环,有多少方案?

这不就组合数吗!

或者也可以按多重集排列数理解,\(x-1\) 个 \(1\),\(y-1\) 个 \(0\) 有多少排列方案。

最终答案是一样的。

所以真正的方程应为:

\[f_i=\sum_{x=1}^{\lfloor \frac{i}{2} \rfloor} f_x\times f_y\times g(x,y)\times C_{x+y-2}^{x-1} \]

最后 dfs 找环贡献加起来就行。

但是这样时间复杂度是 \(O(n^2)\) 的,无法通过。

事实上,\(f\) 有个通项公式:

\[f_n=n^{n-2} \]

这样用快速幂就可以 \(O(n)\) 求出答案了。

等等……为啥是 \(O(n)\)?

设第 \(i\) 个环的长度为 \(l_i\),一共有 \(k\) 个环。

则有 \(\sum_{i=1}^k l_i=n\ge \sum_{i=1}^k \log l_i\)。

所以千万不要预处理 \(f\) 数组!

const int N=1e5+10,Mo=1e9+9;
int n,cnt,col[N],ct[N];
LL inv[N],fac[N];
bool vis[N];
vector<int>e[N];
void dfs(int x) {
	col[x]=cnt;
	for(int y:e[x]) {
		if(col[y]) continue;
		dfs(y);
	}
} 
LL power(LL a,LL b) {
	LL res=1;
	for(;b;b>>=1) {
		if(b&1) res=res*a%Mo;
		a=a*a%Mo;
	}
	return res;
}
void solve() {
	cin>>n;cnt=0;
	FOR(i,1,n) {
		int x=read();
		e[x].pb(i);
	}
	FOR(i,1,n) if(!col[i]) ++cnt,dfs(i);
	FOR(i,1,n) ct[col[i]]++;
	LL ans=fac[n-cnt];
	FOR(i,1,cnt) {
		(ans*=inv[ct[i]-1])%=Mo;
		if(ct[i]<2) continue;
		(ans*=power(ct[i],ct[i]-2))%=Mo;
	}
	cout<<ans<<"\n";
	FOR(i,1,n) e[i].clear(),ct[i]=col[i]=0;
}
main() {
	fac[0]=1;
	FOR(i,1,N-10) fac[i]=fac[i-1]*i%Mo;
	inv[N-10]=power(fac[N-10],Mo-2);
	ROF(i,N-10,1) inv[i-1]=inv[i]*i%Mo;
	int t=read();
	while(t--) solve();
	return 0;
}

最后的问题,\(f_i\) 的通项是怎么来的?

观察可得、oeis大法

证明:

引理:有标号无根树的方案数为 \(n^{n-2}\)(Cayley 公式)。

证明:根据 Prüfer序列 的性质,任何长为 \(n-2\) 的值域为 \([1,n]\) 的序列都可以唯一确定成一棵有标号树。

根据乘法原理,方案数显然为 \(n^{n-2}\)。

证毕。

考虑一个有 \(n\) 个点的环,显然需要 \(n-1\) 次交换就可以分成 \(n\) 个自环。

假设有一张图 \(G\),开始时没有边。

每次若交换 \(p_x,p_y\),则将 \(x,y\) 之间连一条无向边。

显然,这个图不会出现环,否则一定是断开的环之间重连,一定不是最优解。

而又因为该图有 \(n-1\) 条边,所以操作完成后 \(G\) 必定是一棵树。

考虑将操作倒过来,不断让两个环之间交换,最后合成一个大环,这样就转化为在树上删边。

若将树的每条边给一个顺序,按照这个顺序进行删边并合并环,则最后得到的环一定是不变的。

根据引理,边没有顺序的有标号树有 \(n^{n-2}\) 棵,加上边的顺序后就是 \(n^{n-2}\times (n-1)!\)。

这样就把边有标号的环和边有标号的有标号树建立了一一对应关系。

又因为合并顺序不同的环在不考虑合并顺序的差异后均为原环,本质上是一样的,有重复。

所以要去重,要在原来基础上除掉边重复的方案即 \((n-1)!\)。

所以方案数就是 \(n^{n-2}\)。

证毕。

标签:frac,组合,int,res,return,times,初步,数学,pk
From: https://www.cnblogs.com/zengziquan/p/18528661

相关文章

  • 美国高中女生因数学竞赛,发现勾股定理新证明!论文已发《美国数学月刊》
    来源|新智元 ID | AI-era两年前,两位高中在读的学生发现了全新的勾股定理证明方法。遗憾的是,当时并没有更具体的论文,以提供实质性细节。就在最近,两人的全新论文,在《美国数学月刊》上正式发表了!论文地址:https://www.tandfonline.com/doi/full/10.1080/00029890.2......
  • 高等数学,但用我的话来说(征程从函数开始)
    高等数学,但用我的话来说(征程从函数开始)目录‍目录高等数学,但用我的话来说(征程从函数开始)目录函数函数与白盒转换机实心与空心的区间表示法怎么“计算”我们的白盒转换机会做出什么零件垂线检验魔法检验图像是否是函数反函数白盒还原机,回收零件成为材料水平线检验魔法检验一材一......
  • 高等数学,但用我的话来说(征程从函数开始)
    高等数学,但用我的话来说(征程从函数开始)目录‍目录高等数学,但用我的话来说(征程从函数开始)目录函数函数与白盒转换机实心与空心的区间表示法怎么“计算”我们的白盒转换机会做出什么零件垂线检验魔法检验图像是否是函数反函数白盒还原机,回收零件成为材料水平线检验魔法检验一材一......
  • LeetCode题练习与总结:组合总和 Ⅳ --377
    一、题目描述给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。题目数据保证答案符合32位整数范围。示例1:输入:nums=[1,2,3],target=4输出:7解释:所有可能的组合为:(1,1,1,1)......
  • SpringBoot小学生在线数学学习平台m0ncg(程序+源码+数据库+调试部署+开发环境)
    本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表开题报告内容一、研究背景与意义随着互联网技术的普及,在线学习已成为教育领域的重要趋势。小学生作为基础教育阶段的重要群体,其数学学习效果直接影响到后续学业......
  • 操作系统:内核基础实现(三)内存分配的初步实现
    本随笔对应项目代码(更新中):https://github.com/himuhuan/HimuOS位图的实现KrBitMap结构用作任意长度的常规用途一维位图的标头.内核使用位图作为一种经济方式来跟踪一组可重用项。structKrBitMap{uint32_tLength;PRIVATE_DATA_MEMBERBYTE*_buffer;};voidKr......
  • 华为OD机试真题---字母组合
    华为OD机试中的“字母组合”题目是一道涉及字符串处理和回溯算法的编程题。以下是对该题目的详细解析:一、题目描述每个数字关联多个字母,关联关系如下:0关联“a”,“b”,“c”1关联“d”,“e”,“f”2关联“g”,“h”,“i”3关联“j”,“k”,“l”4关联“m”,“n......
  • 数学建模_BP神经网络模型(多输入多输出)回归模型+Matlab代码包教会使用,直接替换数据
    BP神经网络模型(多输入多输出)回归模型神经网络回归模型原理讲解​该模型是一个典型的多层前馈神经网络(FeedforwardNeuralNetwork,FFNN),应用于回归问题。其基本原理包括数据输入、隐藏层神经元的处理、输出层的预测、以及通过反向传播算法优化权重和偏置的过程。下面将详......
  • 【基于PSINS工具箱】以速度为观测量的SINS/GNSS组合导航,CKF滤波
    基于【PSINS工具箱】,提供一个MATLAB例程,仅以速度为观测量的SINS/GNSS组合导航(滤波方式为CKF),无需下载,订阅专栏后可直接复制文章目录工具箱程序简述运行结果代码程序讲解代码功能概述代码结构与关键步骤结论工具箱本程序需要在安装工具箱后使用,工具箱是开源的,链......
  • 基于【PSINS工具箱】提供一个MATLAB例程,仅以速度为观测量的SINS/GNSS组合导航
    基于【PSINS工具箱】,提供一个MATLAB例程,仅以速度为观测量的SINS/GNSS组合导航(滤波方式可选EKF/UKF/CKF),无需下载,订阅专栏后可直接复制文章目录工具箱程序简述运行结果程序源代码程序讲解代码功能概述代码结构与关键步骤结论工具箱本程序需要在安装工具箱后使用,......