关于组合数

定义\({N\choose K}=\frac{N!}{K!(N-K)!}\) 为从 N 个物品中取出 K 个的方案数。

常用公式

  • \(\sum_{i=0}^N \binom{N}{i} = 2^N\).

  • \(\sum_{i=0}^{N} \binom{N}{i}(-1)^i = 0\).

  • \(\sum_{i=0}^{N} \binom{N}{i}x^i = (1+x)^N\).

  • \(\sum_{i=0}^K \binom{N}{i}\binom{M}{K - i} = \binom{N+M}{K}\). 范德蒙公式 \(\sum_{i=0}^{N}{N\choose i}{M\choose i} = \sum_i {N\choose i}{M\choose M-i}={N+M\choose M}\)

  • \(\binom{N}{K}\binom{K}{i} = \binom{N}{i}\binom{N-i}{K-i}\). 例子 \(\sum_k f(i)\sum_i {N\choose K}{K\choose i} = \sum_i{N\choose i}f(i)\sum_K{N-i\choose K-i}=\sum_i f(i){N\choose i} 2^{N-i}\).

  • \(\binom{N}{i} = \binom{N - 1}{i - 1} + \binom{N-1}{i}=\binom{N - 1}{i - 1} + \binom{N-2}{i-1}+\binom{N-2}{i}=...=\sum_{t=i-1}^{N-1} {N-1\choose i-1}\).

  • \(\binom{N+1}{K+1} = \sum_{i=K}^{N} \binom{i}{K}\) (想象 N+1 个物品里选 K+1 个,我枚举第一个被选中的物品是第几个)。

    • 等价地,\(\binom{N+1}{K+1} = \sum_{i=K}^N \binom{i}{i-K}\).
  • \(\sum_{i=0}^{K} {N+1\choose i} =\sum_i {N\choose i}+{N\choose i-1}= 2\sum_{i=0}^{K}{N\choose i} - {N\choose K}\).

[SHOI2015]超能粒子炮·改

求:

\[f(n,k)=\sum_i^k C_n^i \% \ p \]

由卢卡斯定理:

\[\sum_{i=0}^k C_n^i \% \ p= \sum_{i=0}^k C_{n/p}^{i/p} \times C_{n\%p}^{i\%p}\ \% \ p \]

\[=\sum _{r=0}^{p-1}C_{n\%p}^{r} \times \sum_{i=0}^k[i\%p=r] C_{n/p}^{i/p} \]

\[=\sum _{r=0}^{k\%p}C_{n\%p}^{r} \times \sum_{i=0}^{k/p} C_{n/p}^{(i*p+r)/p}+\sum _{r=k\%p+1}^{p-1}C_{n\%p}^{r} \times \sum_{i=0}^{k/p-1} C_{n/p}^{(i*p+r)/p} \]

由于 \(r<p\),有 \((i*p+r)/p=i*p/p=i\),进而:

\[\sum _{r=0}^{k\%p}C_{n\%p}^{r} \times \sum_{i=0}^{k/p} C_{n/p}^{(i*p+r)/p}=\sum _{r=0}^{k\%p}C_{n\%p}^{r} \times \sum_{i=0}^{k/p} C_{n/p}^{i} \\ \sum _{r=k\%p+1}^{p-1}C_{n\%p}^{r} \times \sum_{i=0}^{k/p-1} C_{n/p}^{(i*p+r)/p}=\sum _{r=k\%p+1}^{p-1}C_{n\%p}^{r} \times \sum_{i=0}^{k/p-1} C_{n/p}^{i} \]

因此:

\[f(n,k)=\sum _{r=0}^{k\%p}C_{n\%p}^{r} \times f(n/p,k/p)+\sum _{r=k\%p+1}^{p-1}C_{n\%p}^{r} \times f(n/p,k/p-1) \]

点击查看代码
#include<bits/stdc++.h>
using namespace std;
long long fac[3005],inv[3005];
const long long md=2333;
long long sum[3005][3005];
inline long long C(int x,int y){
	if(x<y)return 0;
	return fac[x]*inv[y]%md*inv[x-y]%md;
}
inline void init(){
	fac[0]=fac[1]=inv[0]=inv[1]=1;
	for(int i=2;i<md;i++)fac[i]=fac[i-1]*i%md;
	for(int i=2;i<md;i++)inv[i]=(md-md/i)*inv[md%i]%md;
	for(int i=2;i<md;i++)inv[i]=inv[i]*inv[i-1]%md;
	for(int i=0;i<md;i++){
		sum[i][0]=1;
		for(int j=1;j<md;j++)sum[i][j]=(sum[i][j-1]+C(i,j))%md;
	}
}

long long solve(long long n,long long k){
	if(k<0)return 0;
	if(n<md&&k<md)return sum[n][k];
	return (sum[n%md][k%md]*solve(n/md,k/md)%md+(sum[n%md][md-1]-sum[n%md][k%md])*solve(n/md,k/md-1)%md)%md;
}
int T;
int main(){
	scanf("%d",&T);init();
	while(T--){
		long long n,k;
		scanf("%lld%lld",&n,&k);
		printf("%lld\n",(solve(n,k)+md)%md);
	}

    return 0;
}

[Code+#3]博弈论与概率统计

类似求卡特兰数的方法,可以算出前缀最小值为 \(-v\) 的方案数为:

\[{{n+m}\choose n+v }-{n+m \choose n+v+1} \]

进而得到,\(n>=m\) 时,得分 \(\in [n-m,n]\),前缀最小值 \(\in [-m,0]\)

\[ans=\sum_{i=0}^{m} (n-m+i)\times ({{n+m}\choose n+i }-{n+m \choose n+i+1}) \\ =(n-m)\times \sum_{i=0}^{m} ({{n+m}\choose n+i }-{n+m \choose n+i+1})+\sum_{i=0}^{m} i\times ({{n+m}\choose n+i }-{n+m \choose n+i+1}) \\ =(n-m)\times {n+m \choose n}+\sum_{i=0}^{m-1}{n+m \choose i} \]

\(n<m\) 时,得分 \(\in [0,n]\),前缀最小值 \(\in [-m,n-m]\)

\[ans=\sum_{i=m-n}^{m} (n-m+i)\times ({{n+m}\choose n+i }-{n+m \choose n+i+1}) \\ =\sum_{i=m-n}^{m} (n-m)\times ({{n+m}\choose n+i }-{n+m \choose n+i+1})+\sum_{i=m-n}^{m} i\times ({{n+m}\choose n+i }-{n+m \choose n+i+1}) \\ =(n-m)\times {n+m \choose n}+\sum_{i=m-n}^{m} i\times {{n+m}\choose n+i }-\sum_{i=m-n}^{m-1} i\times {n+m \choose n+i+1} \\ =(n-m)\times {n+m \choose n}+(m-n)\times {n+m \choose n}+\sum_{i=m-n+1}^{m} i\times {{n+m}\choose n+i }-\sum_{i=m-n}^{m-1} i\times {n+m \choose n+i+1} \\ =\sum_{i=m-n+1}^{m} i\times {{n+m}\choose n+i }-\sum_{i=m-n}^{m-1} i\times {n+m \choose n+i+1} \\ =\sum_{i=m-n+1}^{m} i\times {{n+m}\choose n+i }-\sum_{i=m-n+1}^{m} (i-1)\times {n+m \choose n+i} \\ =\sum_{i=m-n+1}^{m} {{n+m}\choose n+i} \\ =\sum_{i=m+1}^{m+n} {{n+m}\choose i} \\ =\sum_{i=0}^{n-1} {{n+m}\choose i} \]

现在要快速计算形如 \(\sum_{i=0}^{k}\limits {n\choose i}\) 的式子

令:

\[f(n,k)=\sum_{i=0}^{k}{n\choose i} \]

有:

\[f(n,k)=\sum_{i=0}^{k}{n\choose i}=\sum_{i=0}^{k}({n-1\choose i-1}+{n-1\choose i}) \\ =\sum_{i=1}^{k}{n-1\choose i-1}+\sum_{i=0}^{k}{n-1\choose i} \\ =\sum_{i=0}^{k-1}{n-1\choose i}+\sum_{i=0}^{k}{n-1\choose i} \\ =2\times f(n-1,k)-{n-1\choose k} \]

同时:

\[f(n,k)=f(n,k-1)+{n\choose k} \]

\(f(n,k)\)\(f(n-1,k)\)\(f(n,k-1)\) 之间可以 \(O(1)\) 转移,用莫队即可

点击查看代码
#include <bits/stdc++.h>
using namespace std;
int T,p;
long long fac[500005],inv[500005];
const long long md=1e9+7,inv2=5e8+4;
inline void init(){
	fac[0]=fac[1]=inv[0]=inv[1]=1;
	for(int i=2;i<=5e5;i++)fac[i]=fac[i-1]*i%md;
	for(int i=2;i<=5e5;i++)inv[i]=(md-md/i)*inv[md%i]%md;
	for(int i=2;i<=5e5;i++)inv[i]=inv[i]*inv[i-1]%md;
}
inline long long C(int x,int y){
	if(x<y)return 0;
	return fac[x]*inv[y]%md*inv[x-y]%md;
}
long long ans[500005];
int sqr[500005],s;
struct node{
	int n,k,id;
	node(int _n,int _k,int _id){
		n=_n;k=_k;id=_id;
	}
	inline bool operator <(const node &b)const{
		if(sqr[n]!=sqr[b.n])return sqr[n]<sqr[b.n];
		return k<b.k;
	}
};
vector<node> vec;
inline long long pwr(long long x,long long y){
	long long res=1;
	while(y){
		if(y&1)res=res*x%md;
		x=x*x%md;y>>=1;
	}return res;
}
long long res=1,N,K,val[500005];
int main(){
	scanf("%d%d",&T,&p);
	s=500;init();
	for(int i=1;i<=5e5;i++)sqr[i]=i/s+1;
	for(int i=1;i<=T;i++){
		int n,m;scanf("%d%d",&n,&m);
		if(n<m)vec.push_back(node(n+m,n-1,i));
		else{
			ans[i]=(n-m)*C(n+m,n)%md;
			vec.push_back(node(n+m,m-1,i));
		}val[i]=pwr(C(n+m,n),md-2);
	}
	sort(vec.begin(),vec.end());
	for(auto x:vec){
		while(N>x.n)res=inv2*(res+C(--N,K))%md;
		while(K<x.k)res=(res+C(N,++K))%md;
		while(N<x.n)res=(2*res-C(N++,K))%md;
		while(K>x.k)res=(res-C(N,K--))%md;
		ans[x.id]=(ans[x.id]+res)%md;
	}
	for(int i=1;i<=T;i++)printf("%lld\n",(ans[i]+md)%md*val[i]%md);

    return 0;
}

[AGC001E] BBQ Hard

给定正整数数组 \((a_i)_{i=1}^N\)\((b_j)_{j=1}^N\),计算 \(\sum_{i\ne j} \binom{a_i+a_j+b_i+b_j}{a_i+a_j}\),保证 \(N\le 10^5, a_i,b_j\le 2000\)

相当于从 \((-a_i, -b_i)\) 走到 \((a_j, b_j)\),每次向右或者向上一步的方案数。对 i、j 求和相当于需要自选起点 i 和终点 j。做一个DP。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int M=2000,md=1e9+7,inv2=(md+1)>>1;
int n,a[200005],b[200005],maxa,maxb,dp[4005][4005],fac[200005],inv[200005],ans;
bool vis[4005][4005];
inline int DP(int x,int y){
	if(x<-maxa||y<-maxb)return 0;
	if(vis[x+M][y+M])return dp[x+M][y+M];
	vis[x+M][y+M]=1;
	return dp[x+M][y+M]=(1ll*dp[x+M][y+M]+DP(x,y-1)+DP(x-1,y))%md;
}
inline void init(int m){
	fac[0]=1;
	for(int i=1;i<=m;i++)fac[i]=1ll*i*fac[i-1]%md;
	inv[1]=1;
	for(int i=2;i<=m;i++)inv[i]=1ll*(md-md/i)*inv[md%i]%md;
	inv[0]=1;
	for(int i=1;i<=m;i++)inv[i]=1ll*inv[i]*inv[i-1]%md;
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d%d",a+i,b+i);
		maxa=max(maxa,a[i]);
		maxb=max(maxb,b[i]);
		++dp[-a[i]+M][-b[i]+M];
	}
	init(maxa+maxb<<1);
	for(int i=1;i<=n;i++)ans=(ans+1ll*DP(a[i],b[i]))%md;
	for(int i=1;i<=n;i++)ans=(ans-1ll*fac[a[i]+b[i]<<1]*inv[a[i]<<1]%md*inv[b[i]<<1]%md+md)%md;
	printf("%lld",1ll*ans*inv2%md);
	return 0;
}

题3. 有一个 \(N\times N\) 的网格图,其中有 \(K\) 个障碍点。计算从 \((1,1)\) 移动到 \((N,N)\),每次向右或者下走一步,中间不经过任何障碍点的方案数。保证 \(N\le 10^5, K \le 2000\)

Skipped.

加强版:从 \((1, 1, 1)\) 移动到 \((N,M,K)\),每次选择一个维度走。

形如 \((i, i, i)\) 以及 \((N-i,M-i, K-i)\) 的点都是障碍点。求方案数。 \(N,M,K\le 10^5\)

FFT + 生成函数 (多项式求逆)。

Skipped.

题4. (Atcoder - ARC118E) 有一个 \((N+2)\times (N+2)\) 的网格图。对于一个 \(N\) 元排列 \(P\),我们把 \(\{(i,P_i)\}\) 标记为 \(N\) 个障碍点,然后求从 \((0,0)\) 移动到 \((N+1,N+1)\) 的,不经过障碍的路径方案数。对于所有可能的排列 \(P\),求不经过障碍的方案数的总和。 \(N\le 100\).

原题:排列的一部分已经填好,只需要填剩下的位置。

如果排列已经给定的话:容斥:\(f[x][y]\) 表示走到 x、y 这个位置, 经过并标记了偶数个障碍的方案数 - 经过并标记了奇数个障碍方案数。\(f[x][y]\to f[x+1][y]/f[x][y + 1]\)

排列没有给定的话:把生成排列的过程也放进DP 里. \(f[x][y][k][flag]\) 表示在 x、y 这个位置,排列里确定了 k 个元素。flag:当前行 / 列是否已经放了一个障碍。经过并标记了偶数个障碍的方案数 - 经过并标记了奇数个障碍方案数。

枚举 (1) 在当前位置是否放障碍 /(2)向那个方向走。 \(O(N^3)\)

  • 进入一个新的行/列:可以知道当前行列是否已经有障碍。
  • 因为有 flag,所以我们放的障碍一定保证不同行不同列。
  • 我们记录了 k,我们放的障碍个数。这相当于说把排列里 K 个位置填好了。DP结束的时候乘 \((N-k)!\) 来保证生成的是一个排列。

[AGC019F] Yes or No

从一个 \(N\times M\) 的网格图的 \((0,0)\) 移动到 \((N,M)\),经过一个点 \((i,j)\) 的时候可以获得 \(\frac{\max(i,j)}{i+ j}\) 的得分。求所有可能的路径的得分之和。\(N, M\le 10^6\)

\[\sum_{i,j} {i+j\choose i} \binom{N-i+M-j}{N-i}\frac{\max(i,j)}{i+j} \\ =\sum_i \frac{1}{i!(N-i)!} \sum_j \frac{(i+j)!\cdot (N+M-i-j)!}{j!(M-j)!} \frac{i}{i+j}\\ = \sum_i \frac{1}{i!(N-i)!}\sum_j \]

正常做法:从小到大枚举 \(i+j=s\),想计算

\[\sum_{i} \binom{s}{i}\binom{N+M-s}{N-i} \frac{\max(i, j)}{s} \]

我们分别计算

\[A_i(s) =\frac{1}{s} \sum_i \binom{s}{i} \binom{N+M-s}{N-i}\cdot i \cdot [i\ge s/2] . \]

\[A_j(s) =\sum_i \binom{s}{i} \binom{N+M-s}{N-i}\frac{j}{i} \cdot [i < s/2] \]

考虑从 \(s\) 转移到 \(s+1\)

\[A_i(s+1)=\frac{1}{s+1}\sum_{i} \binom{s+1}{i}\binom{N+M-s-1}{N-i}\cdot i\cdot [i\ge (s+1)/2]. \]

\[(s+1)A_i(s+1) - sA_i(s) \\ = \sum_{i} \left( \binom{s}{i} + \binom{s}{i-1} \right) \binom{N+M-s-1}{N-i}\cdot i\cdot [i\ge (s+1)/2] \\ -\sum_i \binom{s}{i}\left( \binom{N+M-s-1}{N-i} +\binom{N+M-s-1}{N-i-1}\right) \cdot i \cdot [i\ge s/2] \]

这里注意到 \(\binom{s}{i}\binom{N+M-s-1}{N-i}\) 可以和 \(\binom{s}{i}\binom{N+M-s-1}{N-i}\) 消掉。\(\binom{s}{i-1}\binom{N+M-s-1}{N-i}\) 可以错位和 \(\binom{s}{i}\binom{N+M-s-1}{N-i-1}\) 消掉。因为只需要特殊计算 \(i\approx (s+1)/2\)\(O(1)\) 项即可。

(可以做 \(O(1)\) 递推)。

神棍做法:不妨设 \(N\ge M\)

\(\frac{\max(i,j)}{i+j}\) 相当于说:还有 \(i\) 个 "yes" 和 \(j\) 个 "no" 在我的前面。我不知道他们出现的顺序,但是我想猜下一个位置是 yes 或者 no。我应该猜个数比较多的,我的成功率就是 \(\frac{\max(i,j)}{i+j}\).

原问题相当于说,从 \(i,j = (n,m)\) 开始,每次猜下一个是 yes 或者 no,猜对了得分,猜错了不得分。问期望的总得分。

如果总是 \(i\ge j\) 的话:我会一直猜 Yes,最后\(n+m\)轮里有 \(n\) 轮猜对,得 \(n\) 分。

如果 \(i=j\)

  • 如果猜错了:\((i,j) => (i, j-1)\) 没有得分,但是仍然有\(i\ge j\)
  • 如果猜对了:\((i,j)=>(i-1,j)\),这个局面和 \((i,j-1)\) 是等价的。所以这里相当于我"白赚"了0.5分。

所以答案是

\[N + \frac{\sum_{i=0}^N{2i\choose i}{N+M-2i\choose N-i}}{2\cdot {N+M\choose N}}. \]

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
long long pwr[1000005],inv[1000005];
const long long md=998244353;
inline void init(){
	inv[0]=inv[1]=pwr[0]=pwr[1]=1;
	for(int i=2;i<=n+m;i++)inv[i]=(md-md/i)*inv[md%i]%md;
	for(int i=2;i<=n+m;i++)inv[i]=inv[i]*inv[i-1]%md;
	for(int i=2;i<=n+m;i++)pwr[i]=pwr[i-1]*i%md;
}
inline long long c(int x,int y){
	return pwr[x+y]*inv[x]%md*inv[y]%md;
}
inline long long powr(long long x,long long y){
	long long res=1;
	while(y){
		if(y&1)res=res*x%md;
		x=x*x%md;y>>=1;
	}return res;
}
long long ans;
int main(){
	scanf("%d%d",&n,&m);
	if(n<m)swap(n,m);init();
	for(int i=1;i<=m;i++)ans=(ans+c(i,i)*c(n-i,m-i))%md;
	ans=ans*powr(c(n,m),md-2)%md;
	ans=(n+ans*inv[2])%md;
	printf("%lld",ans);

	return 0;
}


题6. 组合数取模:计算 \(\binom{N}{M}\bmod P\)

  • 版本 1:\(P= 10^9+7\)\(N, M\le 10^9\).
  • 版本 2:\(P\) 是一个小质数 (Lucas 定理).
  • 版本 3:\(P = 3^{10}\)\(N,M\le 10^{18}\).
  • 版本 3 加强版:\(P=3^{20}\)\(N,M\le 10^{18}\).
posted @ 2021-12-14 15:03  一粒夸克  阅读(153)  评论(0编辑  收藏  举报