组合数学学习报告

本学习报告只涉及很小的一部分的组合数学知识点(估计比选修二也就稍微多一点),更具体和复杂的问题详见:OI Wiki 数学部分,我也会在后面涉及到扩展的时候附上OI Wiki对应页面的链接,以供日后查阅。

组合数学性质和定理

基本定义

加法原理:完成一个工作有n种方案,每一种具体有a[i]个方案,方案数是n个a[i]的加和。
乘法原理:完成一个工作有n个步骤,每一步具体有a[i]个方案,方案数是n个a[i]的乘积。
简而言之,过程中乘法原理,结果中加法原理。

排列:从n个不同元素中,任取m个元素(m≤n)按照一定顺序排成一列,叫做从n个不同元素中取出m个元素的一个排列;从n个不同元素中取出m个元素的所有排列的个数,叫做从n个不同元素中取出m个元素的排列数,表示为 A n m A_n^m Anm.
排列公式:模拟取的过程,第一次从n个里取1个,第二次就是从(n-1)个里取1个,以此类推,第m次就是从(n-m+1)个里取一个,应用乘法原理,则:
A n m = ∏ i = n − m + 1 n i = n ! ( n − m ) ! A_{n}^{m}=\prod_{i=n-m+1}^{n}i=\frac{n!}{(n-m)!} Anm=i=nm+1ni=(nm)!n!
组合:从n个不同元素中,任取m个元素(m≤n),叫作从n个不同元素中取出m个元素的一个组合;从n个不同元素中取出m个元素的所有组合的个数,叫作从n个不同元素中取出m个元素的组合数,表示为 C n m C_n^m Cnm.
组合公式:在排列基础上考虑,对于每一个含m个元素的组合,都在排列中形成全排列,因此应该除以全排列,即:
C n m = A n m m ! = n ! m ! ( n − m ) ! C_{n}^{m}=\frac{A_{n}^{m}}{m!}=\frac{n!}{m!(n-m)!} Cnm=m!Anm=m!(nm)!n!
一般来说,规定m>n时, A n m = C n m = 0. A_n^m = C_n^m = 0. Anm=Cnm=0.

组合数学有关定理

二项式定理:

( a + b ) n = Σ i = 0 n   C n i    a n − i b i (a+b)^n = \Sigma_{i=0}^{n}\,C_{n}^{i}\,\,a^{n-i}b^{i} (a+b)n=Σi=0nCnianibi

二项式定理说明了一个展开式的系数,实际应用很少用到,主要是推导别的定理和性质。

卢卡斯定理:

C n m ≡ C n   m o d   p m   m o d   p ∗ C ⌊ n p ⌋ ⌊ m p ⌋ C_{n}^{m}≡C_{n\,mod\,p}^{m\,mod\,p} * C_{\lfloor\frac{n}{p}\rfloor}^{\lfloor\frac{m}{p}\rfloor} CnmCnmodpmmodpCpnpm
其中要求p是质数,一般p在105左右的时候用来加速求组合数。

组合数的常见性质

1 集合对全集作补集,组合数不变,即 C n m = C n n − m C_{n}^{m}=C_{n}^{n-m} Cnm=Cnnm.

2 根据定义可得, C n m = m n C n − 1 m − 1 . C_n^m=\frac{m}{n}C_{n-1}^{m-1}. Cnm=nmCn1m1.

3 对组合数进行递推,元素总数+1时,假如选新的数,那么从(m-1)转移,否则从m转移,即 C n m = C n − 1 m − 1 + C n − 1 m C_n^m=C_{n-1}^{m-1}+C_{n-1}^{m} Cnm=Cn1m1+Cn1m,用这个递推式能在 O ( n 2 ) O(n^2) O(n2)时间复杂度下求组合数。

4 二项式定理的一个特殊情况: Σ i = 0 n   C n i = 2 n \Sigma_{i=0}^{n}\,C_n^i=2^n Σi=0nCni=2n.

5 组合数的拆分: Σ i = 0 m   C n i C m m − i = C m + n m \Sigma_{i=0}^{m}\,C_{n}^{i}C_{m}^{m-i}=C_{m+n}^{m} Σi=0mCniCmmi=Cm+nm

6 C n r C r k = C n k C n − k r − k C_{n}^{r}C_{r}^{k}=C_{n}^{k}C_{n-k}^{r-k} CnrCrk=CnkCnkrk

其他的一些性质,见OI Wiki 组合数学/排列组合/组合数性质 一节

组合数学有关扩展知识

容斥原理

设全集U中有n种不同属性的元素,每一种构成的集合为 S i S_i Si,那么
∣ ⋃ i = 1 n S i ∣ = ∑ m = 1 n ( − 1 ) m − 1 ∑ a i < a i + 1 ∣ ⋂ i = 1 m S a i ∣ \boxed{\left|\bigcup_{i=1}^{n}S_i\right|=\sum\limits_{m=1}^{n}(-1)^{m-1}\sum\limits_{a_i<a_{i+1}}\left|\bigcap_{i=1}^{m}S_{a_i}\right|} i=1nSi=m=1n(1)m1ai<ai+1i=1mSai
用自然语言解释,就是n个集合的并=每一个集合的加和-两两交集+三三交集-…
扩展:
∣ ⋂ i = 1 n S i ∣ = ∣ U ∣ − ∣ ⋃ i = 1 n S i ‾ ∣ \left|\bigcap_{i=1}^{n}S_i\right|=\left|U\right|-\left|\bigcup_{i=1}^{n}\overline {S_i}\right| i=1nSi=Ui=1nSi
通过几道例题来感受一下容斥原理的应用:
T3 错位排列问题(难度3)

  • 对于一个1~n的排列P,如果满足 P i ≠ i P_i \ne i Pi=i,则称P是n的一个错位排列。给定n,求n的错位排列个数。

这题首先利用补集思想,求错排的个数就相当于从全排列中去掉有任意位满足 P i = i P_i=i Pi=i的个数。定义集合 S i S_i Si表示满足 P i ≠ i P_i \ne i Pi=i的排列的集合,全排列是全集,非法情况就是 ∣ ⋃ i = 1 n S i ‾ ∣ \left|\bigcup\limits_{i=1}^{n}\overline {S_i}\right| i=1nSi.套用容斥原理表达式,问题又变为求一些特定的交集大小,即 ∣ ⋂ i = 1 m S a i ‾ ∣ \left|\bigcap\limits_{i=1}^{m}\overline {S_{a_i}}\right| i=1mSai。考虑它的意义,这个式子的是k个补集的交,也就代表了有m位相同的排列个数,而剩下的部分任意,根据组合数学的思想,有 ∣ ⋂ i = 1 m S a i ‾ ∣ = ( n − m ) ! \left|\bigcap\limits_{i=1}^{m}\overline {S_{a_i}}\right|=(n-m)! i=1mSai=(nm)!,既然如此,设 D n D_n Dn表示n的错排数,那么:
D n = ∣ U ∣ − ∣ ⋃ i = 1 n S i ‾ ∣ = ∣ U ∣ − ∑ m = 1 n ( − 1 ) m − 1 ∑ a 1 , 2 , . . . , m ∣ ⋂ i = 1 m S a i ‾ ∣ = n ! − ∑ m = 1 n ( − 1 ) m − 1   C n m   ( n − m ) ! = n ! − ∑ m = 1 n ( − 1 ) m − 1 n ! m ! = n ! − n ! ∑ m = 1 n ( − 1 ) m − 1 m ! = n ! ∑ m = 1 n ( − 1 ) m m ! . \begin{aligned} D_n &= \left|U\right|-\left|\bigcup\limits_{i=1}^{n}\overline {S_i}\right|\\&=\left|U\right|-\sum\limits_{m=1}^{n}(-1)^{m-1}\sum\limits_{a_{1,2,...,m}}\left|\bigcap_{i=1}^{m}\overline{S_{a_i}}\right|\\&=n!-\sum\limits_{m=1}^{n}(-1)^{m-1}\,C_n^m\,(n-m)!\\&=n!-\sum\limits_{m=1}^{n}(-1)^{m-1}\frac{n!}{m!}\\&=n!-n!\sum\limits_{m=1}^{n}\frac{(-1)^{m-1}}{m!}\\&=n!\sum\limits_{m=1}^{n}\frac{(-1)^{m}}{m!}. \end{aligned} Dn=Ui=1nSi=Um=1n(1)m1a1,2,...,mi=1mSai=n!m=1n(1)m1Cnm(nm)!=n!m=1n(1)m1m!n!=n!n!m=1nm!(1)m1=n!m=1nm!(1)m.

T2 能量采集(洛谷P1447,难度3)
经过一些观察,可以发现, ( x , y ) (x,y) (x,y)的能量损失值恰好等于 g c d ( x , y ) ∗ 2 − 1 gcd(x,y)*2-1 gcd(x,y)21,我们要做的就是对于所有的 ( x , y ) (x,y) (x,y),求出 g c d ( x , y ) ∗ 2 − 1 gcd(x,y)*2-1 gcd(x,y)21的和。
由于x,y都比较大, O ( n 2 ) O(n^2) O(n2)算法是不行的,因此我们转化思路,设 f [ i ] f[i] f[i]表示 g c d ( x , y ) = i gcd(x,y)=i gcd(x,y)=i ( x , y ) (x,y) (x,y)的对数,那么结果就是求 f [ i ] ∗ ( i ∗ 2 − 1 ) f[i]*(i*2-1) f[i](i21)的和。考虑怎么求f,我们先不考虑gcd,假设 g [ i ] g[i] g[i]表示满足 i ∣ g c d ( x , y ) i|gcd(x,y) igcd(x,y) ( x , y ) (x,y) (x,y)的对数,那么 g [ i ] = ( n / i ) ( m / i ) g[i]=(n/i)(m/i) g[i]=(n/i)(m/i).然而f和g的区别就在于, g c d ( x , y ) gcd(x,y) gcd(x,y)可能等于 k ∗ i k*i ki。这下就出现了容斥的形态, g [ i ] g[i] g[i]就是全部的集合,我们一开始令 f [ i ] = g [ i ] f[i]=g[i] f[i]=g[i],然后利用容斥原理的思想,每一次从中去掉 f [ k ∗ i ] f[k*i] f[ki],最终所得的就是真正的f数组。为了实现这种效果,需要倒序枚举i。
代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
//#define int long long
const int N = 1e5 + 1;
long long f[N];
signed main(){
	int i,j,n,m;
	long long res = 0;
	scanf("%d %d",&n,&m);
	if(m < n) swap(m,n);
	for(i = n;i >= 1;i--){
		f[i] = (long long)(n / i) * (m / i);
		for(j = i << 1;j <= n;j += i){
			f[i] -= f[j];
		}
		res += ((i << 1) - 1) * f[i];
	}
	printf("%lld\n",res);
	return 0;
}

这题还有一种用莫比乌斯反演的方法(尽管我没学会莫反看不懂 ),但是显然没有容斥来的容易。对于它的弱化版P2158(n*n方阵),可以 O ( n ) O(n) O(n)求欧拉函数求解,但是不适用于这道题。

T3 硬币购物(洛谷P1450,难度3.5)
乍一看这题就是个完全背包方案统计,然而直接这样做复杂度是 O ( 4 n s ) O(4ns) O(4ns),无法通过。
思考一下这题和传统的完全背包的区别。此题的完全背包,只有4种物品,在每种物品的取法是否合法上,情况仅有16种,非法情况仅有15种,如果我们做一个分类讨论,就能直接从无限背包出发求解,优化成一个接近无限背包的 O ( 4 s ) O(4s) O(4s)的算法。
那么怎么分类讨论?从补集思想考虑,合法的情况就是总情况去除所有的非法情况,总情况数可以用无限背包求解,非法情况我们只需要考虑到取了 d [ i ] + 1 d[i]+1 d[i]+1个物品。因此这题转化成容斥原理的模型,设 f [ i ] f[i] f[i]表示花i元的方案,那么 f [ s ] f[s] f[s]就是全集,求不合法的方案就是求所有不合法情况的并集,答案就是补集。这就可以应用容斥原理求解,而容斥的过程可以递归实现,最终时间复杂度是 O ( 4 s + 2 4 n ) O(4s+2^{4}n) O(4s+24n).
代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1e3 + 1;
const int M = 1e5 + 1;
long long dp[M],c[5],d[5],res;
void dfs(int x,int sum,int p){
	if(sum < 0) return;//花费溢出
	if(x > 4){
		res += dp[sum] * p;//计入一种非法情况
		return;
	}
	dfs(x + 1,sum,p);//合法
	dfs(x + 1,sum - c[x] * (d[x] + 1),-p);//不合法
}
int main(){
	int i,j,t,s;
	for(i = 1;i <= 4;i++) scanf("%d",&c[i]);
	scanf("%d",&t);
	dp[0] = 1;
	for(i = 1;i <= 4;i++){
		for(j = c[i];j < M;j++){
			dp[j] += dp[j - c[i]];
		}
	}
	while(t--){
		res = 0;
		for(i = 1;i <= 4;i++) scanf("%d",&d[i]);
		scanf("%d",&s);
		dfs(1,s,1);//全集是要的,所以初始情况下p=1
		printf("%lld\n",res);
	}
	return 0;
}

T4 过河卒二(洛谷P5376,难度4)
首先不考虑那几个特殊位置,对于移动的过程,我们需要做到的就是用一定的步数横向移动m个单位、纵向移动n个单位。假设过程中沿对角线移动了i步,这i步在哪一步移动可以任意分配;除此之外还需要走(n+m-2*i)步,其中需要在纵向上移动(n-i)步,同理,这m步可以任意分配,那么移动的方案数就应该为:
∑ i = 0 m i n ( n , m ) C n + m − i i × C n + m − 2 × i n − i \sum\limits_{i=0}^{min(n,m)}C_{n+m-i}^{i}\times C_{n+m-2\times i}^{n-i} i=0min(n,m)Cn+mii×Cn+m2×ini
(考虑到此题模数较小而n m又比较大,这个求组合数的过程需要用卢卡斯定理)
现在开始考虑走到了特殊位置的情况,首先为了便于处理,把起点和终点(走到棋盘外直接视作走到(n+1,m+1),因为走到边缘的时候移动方案唯一)也视作特殊点,答案应该为总的移动方案-经过了至少一个特殊点的移动方案,后者很明显可以容斥求解,求解需要利用状压来实现。具体的说,先把所有点递增的排序以保证能顺序经过,状态当中必须含有起止点,除此之外,每多选一个点就应该把正负翻转一次(这个看式子不难想),选了x个点就相当于依次经过这些点的方案组合。
总之此题非常综合,比较难做,算是容斥和状压结合的一个基础(不得不说容斥+状压确实才是真正的容斥,但是也确实很阴间)。
代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define int long long
const int N = 1e5 + 1;
const int mod = 59393;
struct yjx{
    int x,y;
}a[23];
bool cmp(yjx p,yjx q){
    if(p.x != q.x) return p.x < q.x;
    return p.y < q.y;
}
int n,m,p,k,num[23];
long long fac[N],inv[N],f[23][23];
long long ksm(long long a,int b){
    long long ret = 1;
    while(b){
        if(b & 1) ret = ret * a % mod;
        a = a * a % mod;
        b >>= 1;
    }
    return ret;
}
long long C(int n,int m){
    if(m > n) return 0;
    return fac[n] * inv[m] % mod * inv[n - m] % mod;
}
long long lucas(int n,int m){
    if(!m) return 1;
    return lucas(n / mod,m / mod) * C(n % mod,m % mod) % mod;
}
long long val(int x,int y){
    int i,t = min(x,y),ret = 0;
    if(t < 0) return 0;
    for(i = 0;i <= t;i++){
        ret = (ret + lucas(x + y - i,i) * lucas(x + y - 2 * i,x - i) % mod) % mod;
    }
    return ret;
}
long long solve(int st){
    if(!(st & 1) || !(st & (1 << (k - 1)))) return 0;//判断是否包含起止点
    int i,cnt = 0;
    long long ret = 1;
    memset(num,0,sizeof(num));
    for(i = 1;i <= k;i++){
        if(st & (1 << (i - 1))){
            num[++cnt] = i;
            ret = -ret;
        }
    }
    for(i = 1;i < cnt;i++){
        ret = ret * f[num[i]][num[i + 1]] % mod;
        //经过集合S中的点的方案数累乘
    }
    return (ret + mod) % mod;
}
signed main(){
    int i,j;
    long long res = 0;
    scanf("%lld %lld %lld",&n,&m,&k);
    ++n,++m;
    for(i = 1;i <= k;i++) scanf("%lld %lld",&a[i].x,&a[i].y);
    a[k + 1].x = 1,a[k + 1].y = 1,a[k + 2].x = n,a[k + 2].y = m;
    k += 2;
    sort(a + 1,a + k + 1,cmp);
    fac[0] = 1;
    for(i = 1;i < mod;i++){
        fac[i] = fac[i - 1] * i % mod;
    }
    inv[mod - 1] = ksm(fac[mod - 1],mod - 2);
    for(i = mod - 2;i >= 0;i--){
        inv[i] = inv[i + 1] * (i + 1) % mod;
    }
    for(i = 1;i <= k;i++){
        for(j = i + 1;j <= k;j++){
            f[i][j] = val(a[j].x - a[i].x,a[j].y - a[i].y);//注意别减错了...
        }
    }
    for(i = 0;i < (1 << k);i++){
        res = (res + solve(i)) % mod;
    }
    printf("%lld\n",res);
    return 0;
}  

T5 幸运数字(洛谷P2567,难度4.5)
此题首先可以预处理所有的幸运数字。
首先,对于一个幸运数字 x x x,它在 [ L , R ] [L,R] [L,R]内出现的次数是 ⌊ R x ⌋ − ⌊ L − 1 x ⌋ \lfloor\frac{R}{x}\rfloor-\lfloor\frac{L-1}{x}\rfloor xRxL1,对于所有的集合数字,出现重复计算的部分就是几个数字的 l c m lcm lcm的倍数,所以我们需要进行搜索,求出所有的 l c m lcm lcm并统计答案。
问题是幸运数太多了,搜索必须剪枝。
首先比较容易想到的是,很多幸运数字能被其它的幸运数字整除,那这些数字可以直接删了,考虑到其中能整除6和8的绝对不在少数,所以优化幅度很大。另外,为了减少搜索层数,可以从大到小对剩下的幸运数字进行排序,一旦溢出区间直接停止搜索。
剩下这个就稍微难想一点:我们为了求解 l c m lcm lcm,使用的是 l c m ( x , y ) × g c d ( x , y ) = x y lcm(x,y)\times gcd(x,y)=xy lcm(x,y)×gcd(x,y)=xy的方法。如果一个幸运数很大,和任何剩下的数求 l c m lcm lcm都会溢出,那么这个数就只会产生正的贡献一次,不必加到搜索当中,这个大小的分界线可以是 R 2 \frac{R}{2} 2R(事实上 R 3 \frac{R}{3} 3R也可以,因为较大的数的 g c d gcd gcd不会小于3)。这一来又去掉了很多情况。
进行上述剪枝之后,就能通过此题了。
代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define int long long
const int mod = 1e9 + 7;
int n,m,cnt;
long long L,R,num[5001],res;
bool vis[5001];
bool cmp(long long x,long long y){
    return x > y;
}
void find(long long sum){//求幸运数
    if(sum > R) return;
    find(sum * 10 + 6);
    find(sum * 10 + 8);
    if(sum) num[++cnt] = sum;
}
void check(){//删去不需要搜索的数
    int i,j;
    sort(num + 1,num + cnt + 1);
    for(i = 1;i <= cnt;i++){
        for(j = 1;j < i;j++){
            if(num[i] % num[j] == 0){
                vis[i] = 1;
                break;
            }
        }
    }
    for(i = 1;i <= cnt;i++){
        if(!vis[i]){
            if(num[i] <= R / 2) num[++m] = num[i];
            else res += R / num[i] - L / num[i];
        }
    }
}
long long gcd(long long a,long long b){
    if(!b) return a;
    return gcd(b,a % b);
}
void dfs(int x,int y,long long sum){
    if(x == m + 1){
        if(sum ^ 1) res += y * (R / sum - L / sum);//sum=1的时候一个数都没有取
        return;
    }
    dfs(x + 1,y,sum);
    long long temp = num[x] / gcd(num[x],sum);//求出lcm
    long long a,b;
    a = sum / mod,b = temp / mod;
    if(a * b == 0 && sum * temp <= R) dfs(x + 1,-y,sum * temp);
    //此处a*b是为了防止溢出long long
}
signed main(){
    int i;
    scanf("%lld %lld",&L,&R);
    --L;
    find(0);
    check();
    sort(num + 1,num + m + 1,cmp);
    dfs(1,-1,1);
    printf("%lld\n",res);
    return 0; 
}

对于更多容斥原理的介绍,见OI Wiki 组合数学/容斥原理一节。

抽屉原理

  • 将(n+1)个物品,划分为n组,那么至少有一组有2个物品。

这就是抽屉原理。抽屉原理还有以下的几个扩展:

  • 将(nm+1)个物品,划分为n组,那么至少有一组有m个物品。
  • 将nm个物品,划分为n组,那么至少有一组有m个或以上的物品。
  • 将无穷多个物品,划分为n组,那么至少有一组有无穷多个物品。

抽屉原理看似非常浅显全是废话 ,但这也代表着真正应用的时候抽屉原理不容易被想起。事实上,抽屉原理在某些题目中非常有用,之后就会见到一道应用这个原理的题目。

隔板法

  • 将n个完全相同的小球,放在m个不同的盒子里,每个盒子至少放一个球,求放球的方案总数。

这就是隔板法求解的组合数学问题的经典模型。对于上面这样的问题,可以转化为把n个球用(m-1)个板划分为m组的总方案数,板有(n-1)个空位可以选择,故答案等于 C n − 1 m − 1 C_{n-1}^{m-1} Cn1m1.
隔板法求解的问题必须具有的条件是:所有的元素性质相同(即元素无特殊限制),不能有一组为空,所有的集合互不相同。

T6-1 方程的解(难度1.5)

  • 求不定方程 a 1 + a 2 + ⋯ + a k = n ( ∀ i ∈ [ 1 , n ] , a i > 0 ) a_1+a_2+\cdots+a_k=n(\forall i \in [1,n],a_i>0) a1+a2++ak=n(i[1,n],ai>0)解集的个数。 例:n=4,k=3,所有的解集为{1,1,2}{1,2,1}{2,1,1}。

这道题转化一下思路,把n看作n个1,那么问题就变成把n个1分为k组且每组不为空的方案数,答案就是 C n − 1 k − 1 C_{n-1}^{k-1} Cn1k1.

T6-2 方程的解(难度2)

  • 求不定方程 a 1 + a 2 + ⋯ + a k = n ( ∀ i ∈ [ 1 , n ] , a i ≥ 0 ) a_1+a_2+\cdots+a_k=n(\forall i \in [1,n],a_i\geq0) a1+a2++ak=n(i[1,n],ai0)解集的个数。

这道题和T4-1的唯一区别在于每一组可以不分到1.为了用隔板法求解,假设每一组自带一个1,也就是一共(n+k)个1,再分为k组,这样又是一个隔板法可以求解的问题,答案就是 C n + k − 1 k − 1 C_{n+k-1}^{k-1} Cn+k1k1.

T6-3 方程的解(难度2)

  • 求不定方程 a 1 + a 2 + ⋯ + a k = n ( ∀ i ∈ [ 1 , n ] , a i ≥ i ) a_1+a_2+\cdots+a_k=n(\forall i \in [1,n],a_i\geq i) a1+a2++ak=n(i[1,n],aii)解集的个数。

这个问题和T4-2类似,先在k组中放0,1,2,…(k-1)一共 k ( k − 1 ) 2 \frac{k(k-1)}{2} 2k(k1)个1,剩下的分为k组,答案是 C n − k ( k − 1 ) 2 − 1 k − 1 C_{n-\frac{k(k-1)}{2}-1}^{k-1} Cn2k(k1)1k1.

注意这两问的不同,这一问当中预设了每一组的1的个数,这些1不能参与分组,所以从n个1当中扣除;但是上一问我们只是希望进行转化,分完的组每组去掉一个就是正解,所以加入的1参与分组。是否把加入的参与分组,我认为通过得到一组解之后怎么处理回原来的问题判断比较容易,后处理的参与,先处理的不参与。

T6-4(难度2)

  • 求不定方程 a 1 + a 2 + ⋯ + a k ≤ n ( ∀ i ∈ [ 1 , n ] , a i ≥ 0 ) a_1+a_2+\cdots+a_k\le n(\forall i \in [1,n],a_i\geq 0) a1+a2++akn(i[1,n],ai0)解集的个数。

这题相比T4-2,一个变化在于可以不把(n+m)个1全部取完。为了转化回去,我们假设多开一组用来存取完之后剩下的1,这一组也可以不放。现在问题就变为n个1分为(m+1)个组且组可以为空,那么答案是 C n + m m C_{n+m}^{m} Cn+mm.

组合数学分析问题思路总结

组合数学的难点我认为主要是以下两个:分析出问题的表达式和化简表达式以降低数据规模。接下来就从这两方面进行总结。

分析表达式

T7 数列计数(难度3)

  • 求出有多少长度为n的单调不降或单调不增数列,满足所有的数均在[1,n]内。(n<=1e6)

首先,我们只考虑单调不降一种情况,最后*2就完事了。
这个数列是我们自己安排的,所以只要我们选了数,那必然能构成一个单调不降序列。我们可选的数有[1,n]个,假设我们选i个数,最后数列应该是1,1,…2,2,…i,i,i的形式,所以就相当于把n个位置分成i段,每一段填一个我们选的数,用隔板法,方案有 C n − 1 i − 1 C_{n-1}^{i-1} Cn1i1种。再考虑选数,从n个里面选i个填进去,方案有 C n i C_n^i Cni种。所以答案就是 2 ∗ ∑ i = 1 n C n i   C n − 1 i − 1 2*\sum\limits_{i=1}^{n}C_{n}^{i}\,C_{n-1}^{i-1} 2i=1nCniCn1i1.考虑到n的大小,我们先递推预处理阶乘和阶乘的逆元(处理方法以前有总结过,先费马小定理求一个最大的数的阶乘的逆元,然后递推处理),就可以 O ( 1 ) O(1) O(1)求组合数,过掉此题。

T8 FFF(洛谷P4931)(难度4.5)

g [ n ] g[n] g[n]表示n对情侣错开的方案数,先考虑怎么求g数组。这个问题我们要借用上古时期错排问题的思路(传送门:YBT P1 递归与递推),假设有一排坐着的人不是一对,如果他们的情侣坐在一起,那么剩下的人进行错排,则应该从 g [ i − 2 ] g[i-2] g[i2]推出结果,更具体的来说,1对出现在i排,那么他们的情侣可以坐的排数是(i-1)排,他们的情侣还可以交换过来坐,因此 g [ i ] + = 2 ( i − 1 ) ∗ g [ i − 2 ] g[i]+=2(i-1)*g[i-2] g[i]+=2(i1)g[i2];如果他们的情侣不坐在一起,为了和上一种情况作区分,我们把他们的情侣再看作一对情侣,那么这个问题变为(i-1)个人的错排。而从2i个人里面选出不是情侣的2个人的方案数就是 2 i ∗ ( 2 i − 2 ) 2i*(2i-2) 2i(2i2)(先选一个人,然后从不是他情侣的剩下(2i-2)个里面再选一个)。综上, g [ i ] = 2 i ∗ ( 2 i − 2 ) ∗ ( g [ i − 1 ] + 2 ( i − 2 ) ∗ g [ i − 2 ] ) g[i]=2i*(2i-2)*(g[i-1]+2(i-2)*g[i-2]) g[i]=2i(2i2)(g[i1]+2(i2)g[i2]).
R k R_k Rk表示k对情侣坐在一起的方案数,考虑有哪些元素要参与方案数的计算:首先剩下的(n-k)对错排,是 g [ n − k ] g[n-k] g[nk];在n对里面选k对,是 C n k C_n^k Cnk,这k对人可以坐在这n行的任意行,是 A n k A_n^k Ank;最后,这k对座位可以左右交换,是 2 k 2^k 2k.综上所述:
R k = C n k ∗ A n k ∗ 2 k ∗ g [ n − k ] . R_k=C_n^k*A_n^k*2^k*g[n-k]. Rk=CnkAnk2kg[nk].
此题就结束了。

纵观这两道题,我们得出答案的时候,首先需要注意到所有的情况的分支,例如T5既要考虑选数的方案数,又要考虑填数的方案数;另外要严谨地考虑问题,尤其是考虑取出一些量之后剩下的部分的情况,例如T6的错排分类讨论。

化简表达式

化简的方法有很多,比较常见的是利用组合数自身的性质或者按定义把组合数拆成累乘化简。

T9 随机问题 (难度4)
有m个[0,2n)内均匀分布的随机变量,求至少有两个变量取值相同的概率。
为了避免精度误差,假设你的答案可以表示成 a b \frac{a}{b} ba的形式(gcd(a,b)=0或者a=b),你需要输出a和b对1e6+3取模后的值。(n,m<=1e18)

看完数据范围不要慌张,先喝口水冷静一下,推导一下表达式。
这题显然得补集思想,把答案表示为1-P(任何两个变量取值不同),和前面T5一样,只要我们选出m个变量,就一定不重复,那么任何两个变量取值不同的概率就可以表示为 A 2 n m 2 n m \frac{A_{2^n}^m}{2^{nm}} 2nmA2nm,则 a = 2 n m − A 2 n m , b = 2 n m a=2^{nm}-A_{2^n}^m,b=2^{nm} a=2nmA2nm,b=2nm.
现在考虑一下怎么求,求解麻烦主要在于这个 A 2 n m = ∏ i = 2 n − m + 1 2 n i A_{2^n}^m=\prod_{i=2^n-m+1}^{2^n}i A2nm=i=2nm+12ni。事实上,这个累乘的过程中遍历了一个长度为m的区间,如果m>=mod,在这个区间中,至少有一个数能被mod整除(抽屉原理),那么直接a=b。因此这样线性的 O ( m i n ( 1 e 6 + 3 , m ) ) O(min(1e6+3,m)) O(min(1e6+3,m))的算法是可以的。
现在考虑下一个问题,那就是怎么对a和b约分。
显然, g c d ( a , b ) = 2 x ( x ≤ m n ) gcd(a,b)=2^x(x \le mn) gcd(a,b)=2x(xmn)。现在的问题就在于求出这个x。
另外,我们可以推导得出,m和2n-m当中2的次数相同

  • 证明:设a=m,b=2n-m,那么a+b=2n。设a=2x*p,b=2y*q(p,q为奇数),假设x<y,那么等式两侧同时除以2x,得p+2y-x*q=2n-x,由于p为奇数,等式不成立,因此x=y,得证。

把这个性质从2n-m一直推广到推广2n,可以得到,从 ∏ i = 2 n − m + 1 2 n i \prod_{i=2^n-m+1}^{2^n}i i=2nm+12ni当中约去2x也就相当于从 ( m − 1 ) ! (m-1)! (m1)!约去2x,而这里又有一个性质: ( m − 1 ) ! (m-1)! (m1)!中2的次数等于 ∑ i = 1 2 i < = m ⌊ m 2 i ⌋ \sum_{i=1}^{2^i<=m}\lfloor\frac{m}{2^i}\rfloor i=12i<=m2im,这就能在 O ( l o g m ) O(logm) O(logm)的时间内求出x,最后做一个逆元就行了。

吐槽一句,这道题来源里面题面不说a=b的时候必须输出a b,把2n打成2n,我也是服了…

代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
const int mod = 1e6 + 3;
long long ksm(long long a,long long b){
	long long ret = 1;
	while(b){
		if(b & 1) ret = ret * a % mod;
		a = a * a % mod;
		b >>= 1;
	}
	return ret;
}
int main(){
	long long i,a,b,c,n,m,p,cnt = 0;
	scanf("%lld %lld",&n,&m);
	for(i = 2ll;i <= n;i <<= 1){
		++cnt;
	}
	if(cnt >= n){
		printf("1 1\n");
		return 0;
	}
	a = 1;
	c = n;
	p = ksm(2,n);
	b = ksm(p,m);
	for(i = 2ll;i < m;i <<= 1){
		c += (m - 1) / i;
	}
	c = ksm(ksm(2,c),mod - 2);//求约数的逆元
	if(m <= mod){
		a = c;
		for(i = 0;i < m;i++){
			a = a * (p - i + mod) % mod;//正常求解
			//printf("%lld\n",a);
		}
	}
	else a = 0;
	b = b * c % mod;
	a = (b - a + mod) % mod;
	printf("%lld %lld\n",a,b);
	return 0;
} 

在此题当中,我们就运用了多条性质和定理(尽管有一些比较罕见,没有在开头那里列出来)成功压缩了时间复杂度。这道题的启示就是,在组合数学问题中,如果数据规模比较大,时间上连线性算法都不能解决,可以从性质上下手优化。

综合运用

T10 打牌概率

  • 有一群人围在一起打牌,这副牌有n张,其中有k张A,每个人抽m张牌。试求解下面两个问题:
  • (1)已知一个人手里有一张A,那么他手中有至少2张A的概率是多少?
  • (2)在(1)的基础上,已知这个人手里的是一张黑桃A(黑桃A仅有一张),那么他手中有至少两张A的概率又是多少?
  • m,n<=1e6,k<=2500。输出精确到小数点后4位。

    我们首先分析一下(1)。还是利用补集思想,答案就是1-P(手中只有一张A)。P的分母应该是手中有A的情况,即全部情况-手中无A的情况,也就是 C n m − C n − k m C_n^m-C_{n-k}^m CnmCnkm,分子就是有任意一张A,即 k ∗ C n − k m − 1 k*C_{n-k}^{m-1} kCnkm1.答案即为: 1 − k ∗ C n − k m − 1 C n m − C n − k m 1-\frac{k*C_{n-k}^{m-1}}{C_n^m-C_{n-k}^m} 1CnmCnkmkCnkm1
    很显然,这个数的分母和分子都极大,我们直接求只能先算组合数后约分,而算组合数绝对会爆long long,所以必须进行化简。这个式子化简没有任何技巧可言,就是把组合数用阶乘表示,然后尝试约分。约分的目标就是尽可能多约掉几项(尤其是含n,m的)来降低数据大小。

从开始做这道题到算出这个的答案,花了我一个多小时。
(过程参考会在代码后给出)

f ( x , y ) = ∏ i = x y i f(x,y)=\prod_{i=x}^{y}i f(x,y)=i=xyi,最终化简的结果表示如下:

1 − k m ( n − m − k + 1 ) ( f ( n − k + 1 , n ) f ( n − m − k + 1 , n − m ) − 1 ) 1-\frac{km}{(n-m-k+1)(\frac{f(n-k+1,n)}{f(n-m-k+1,n-m)}-1)} 1(nmk+1)(f(nmk+1,nm)f(nk+1,n)1)km

对于这两个 f f f,我们发现这两者经过的区间长度都为m,所以可以直接循环内累乘比值。

现在来考虑(2)。还是利用补集思想,答案=1-P(手里只有一张A且是黑桃A),对于这个P,它与(1)的区别在于知道是哪一张,分母就是手中有黑桃A,黑桃A是特定的一张牌,这就相当于从剩下的(n-1)张牌里摸(m-1)张,那么就应该是 C n − 1 m − 1 C_{n-1}^{m-1} Cn1m1,而分子相应的应该是 C n − k m − 1 C_{n-k}^{m-1} Cnkm1,答案即为:
1 − C n − k m − 1 C n − 1 m − 1 1-\frac{C_{n-k}^{m-1}}{C_{n-1}^{m-1}} 1Cn1m1Cnkm1
这个相比(1)就好化简非常非常多了,最终化简结果表示如下(定义同上):
f ( n − m − k + 2 , n − m ) f ( n − k + 1 , n − 1 ) \frac{f(n-m-k+2,n-m)}{f(n-k+1,n-1)} f(nk+1,n1)f(nmk+2,nm)

处理这个式子的方法也和上面一样。

这题的代码我写了一共只有25行,什么叫浓缩的都是精华…
以及这道题的原题解非常非常非常非常离谱,根本不化简,初级表达式写的还不对…

代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int main(){
	int i;
	double n,m,k;
	double temp = 1,x,y;
	scanf("%lf %lf %lf",&n,&m,&k);
	x = n - k + 1,y = n - m - k + 1;
	for(i = 1;i <= k;i++){
		temp = temp * (x / y);//直接同时累乘和算分数
		++x,++y;
	}
	temp = (temp - 1) * (n - m - k + 1);
	printf("%.4lf\n",1 - k * m / temp);
	temp = 1;
	x = n - m - k + 2,y = n - k + 1;
	for(i = 1;i < k;i++){
		temp = temp * (x / y);//同上
		++x,++y;
	}
	printf("%.4lf\n",1 - temp);
	return 0;
} 

化简过程如下(仅供参考):
(1)
P = C n − k m − 1 C n m − C n − k m , P = ( n − k ) ! ( m − 1 ) !   ( n − m − k + 1 ) ! n ! m !   ( n − m ) ! − ( n − k ) ! m !   ( n − m − k ) ! = ( n − k ) !   ( n − m ) !   ( n − m − k ) !   m ! ( m − 1 ) !   ( n − m − k + 1 ) !   ( n !   ( n − m − k ) ! − ( n − k ) !   ( n − m ) ! ) = m ( n − k ) !   ( n − m ) ! ( n − m − k + 1 ) ( n !   ( n − m − k ) ! − ( n − k ) !   ( n − m ) ! ) = m ( n − k ) ! ( n − m − k + 1 ) ( n ! f ( n − m − k + 1 , n − m ) − ( n − k ) ! ) = m ( n − m − k + 1 ) ( f ( n − k + 1 , n ) f ( n − m − k + 1 , n − m ) − 1 ) , ∴ a n s = 1 − k P = 1 − k m ( n − m − k + 1 ) ( f ( n − k + 1 , n ) f ( n − m − k + 1 , n − m ) − 1 ) . \begin{aligned} P&=\frac{C_{n-k}^{m-1}}{C_n^m-C_{n-k}^m},\\ P&=\frac{\frac{(n-k)!}{(m-1)!\,(n-m-k+1)!}}{\frac{n!}{m!\,(n-m)!}-\frac{(n-k)!}{m!\,(n-m-k)!}}\\ &=\frac{(n-k)!\,(n-m)!\,(n-m-k)!\,m!}{(m-1)!\,(n-m-k+1)!\,(n!\,(n-m-k)!-(n-k)!\,(n-m)!)}\\ &=\frac{m(n-k)!\,(n-m)!}{(n-m-k+1)(n!\,(n-m-k)!-(n-k)!\,(n-m)!)}\\ &=\frac{m(n-k)!}{(n-m-k+1)(\frac{n!}{f(n-m-k+1,n-m)}-(n-k)!)}\\ &=\frac{m}{(n-m-k+1)(\frac{f(n-k+1,n)}{f(n-m-k+1,n-m)}-1)},\\ \therefore ans&=1-kP\\ &=1-\frac{km}{(n-m-k+1)(\frac{f(n-k+1,n)}{f(n-m-k+1,n-m)}-1)}. \end{aligned} PPans=CnmCnkmCnkm1,=m!(nm)!n!m!(nmk)!(nk)!(m1)!(nmk+1)!(nk)!=(m1)!(nmk+1)!(n!(nmk)!(nk)!(nm)!)(nk)!(nm)!(nmk)!m!=(nmk+1)(n!(nmk)!(nk)!(nm)!)m(nk)!(nm)!=(nmk+1)(f(nmk+1,nm)n!(nk)!)m(nk)!=(nmk+1)(f(nmk+1,nm)f(nk+1,n)1)m,=1kP=1(nmk+1)(f(nmk+1,nm)f(nk+1,n)1)km.
(2)
Q = C n − k m − 1 C n − 1 m − 1 , Q = ( n − k ) ! ( m − 1 ) !   ( n − m − k + 1 ) ! ( n − 1 ) ! ( m − 1 ) !   ( n − m ) ! = ( n − k ) !   ( n − m ) !   ( m − 1 ) ! ( n − m − k + 1 ) !   ( n − 1 ) !   ( m − 1 ) ! = ( n − k ) !   ( n − m ) ! ( n − m − k + 1 ) !   ( n − 1 ) ! = ( n − k ) !   f ( n − m − k + 2 , n − m ) ( n − 1 ) ! = f ( n − m − k + 2 , n − m ) f ( n − k + 1 , n − 1 ) ∴ a n s = 1 − Q = 1 − f ( n − m − k + 2 , n − m ) f ( n − k + 1 , n − 1 ) . \begin{aligned} Q&=\frac{C_{n-k}^{m-1}}{C_{n-1}^{m-1}},\\ Q&=\frac{\frac{(n-k)!}{(m-1)!\,(n-m-k+1)!}}{\frac{(n-1)!}{(m-1)!\,(n-m)!}}\\ &=\frac{(n-k)!\,(n-m)!\,(m-1)!}{(n-m-k+1)!\,(n-1)!\,(m-1)!}\\ &=\frac{(n-k)!\,(n-m)!}{(n-m-k+1)!\,(n-1)!}\\ &=\frac{(n-k)!\,f(n-m-k+2,n-m)}{(n-1)!}\\ &=\frac{f(n-m-k+2,n-m)}{f(n-k+1,n-1)}\\ \therefore ans&=1-Q=1-\frac{f(n-m-k+2,n-m)}{f(n-k+1,n-1)}. \end{aligned} QQans=Cn1m1Cnkm1,=(m1)!(nm)!(n1)!(m1)!(nmk+1)!(nk)!=(nmk+1)!(n1)!(m1)!(nk)!(nm)!(m1)!=(nmk+1)!(n1)!(nk)!(nm)!=(n1)!(nk)!f(nmk+2,nm)=f(nk+1,n1)f(nmk+2,nm)=1Q=1f(nk+1,n1)f(nmk+2,nm).

总而言之,组合数学是数学当中的一个大项目,知识点多而且琐碎,无论是从思维含量上还是从技巧、代码难度上都相当高,要善于运用组合数自身的性质,严谨地讨论和求解,同时兼顾基础的计算和化简。

对于更多有关组合数学的知识,详见OI Wiki.

Thank you for reading!

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值