关于组合数
定义:\({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]超能粒子炮·改
求:
由卢卡斯定理:
由于 \(r<p\),有 \((i*p+r)/p=i*p/p=i\),进而:
因此:
点击查看代码
#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\) 时,得分 \(\in [n-m,n]\),前缀最小值 \(\in [-m,0]\):
\(n<m\) 时,得分 \(\in [0,n]\),前缀最小值 \(\in [-m,n-m]\):
现在要快速计算形如 \(\sum_{i=0}^{k}\limits {n\choose i}\) 的式子
令:
有:
同时:
\(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\)。
正常做法:从小到大枚举 \(i+j=s\),想计算
我们分别计算
和
考虑从 \(s\) 转移到 \(s+1\):
这里注意到 \(\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分。
所以答案是
点击查看代码
#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}\).