关于组合计数

组合计数在高中阶段是数学的一个必修的章节,难度不大。所以你以为的组合计数题仅仅是像这样对吗?

[HNOI2008]越狱

题目描述

监狱有 n个房间,每个房间关押一个犯人,有 m 种宗教,每个犯人会信仰其中一种。如果相邻房间的犯人的宗教相同,就可能发生越狱,求有多少种状态可能发生越狱。

答案对 100,003取模。

这个问题很简单对吧,直接用等价转换法和乘法计数原理就可做出来了。

但是和数论结合的组合计数才是真正的硬菜:

AcWing 213.古代猪文(SDOI2010)

同样是省选考察组合计数,为啥过了两年SD的就比HN的难了这么多呢?

重要引理:Lucas定理:C_{n}^{m}\equiv C_{n/p}^{m/p}C_{n\: mod\:p}^{m\:mod\:p}(mod\, p)

这题可以很容易想到以下几步操作:看到幂的形式,想到欧拉定理的推论,直接将指数对\varphi \left ( 999911659 \right )取模就行。然后通过试除法来枚举n的约数d,然后一个个用Lucas定理求出取模后的值相加得到指数,再用一次快速幂即可。但是由于999911659是质数,\varphi \left ( 999911659 \right )=

999911658。这个模数太过于巨大,会导致Lucas定理退化成O(n)的时间复杂度,不能接受。所以接下来就是重头戏:优化。

1.对于999911658的分析:将这个数分解质因数得到:999911658=2*3*4679*35617。这个分解式有一个很明显的特点就是:所有因子的次数都是1。在这里考虑到用CRT。

思路就是将2、3、4679、35617分别作为模数求出指数取模后的值分别设为a1,a2,a3,a4,然后求解一个线性同余方程组即可。

这个题目还是主要来考察CRT的运用啊,我们还是要先去想到缩小模数,对于很大的模数进行分解质因数,发现特殊的性质,便想到采用CRT。(对CRT求出的通解的实质理解要深刻)

代码如下: 

#include<iostream>
using namespace std;
const int mod=999911658;
typedef long long ll;
ll a[5],b[5]={0,2,3,4679,35617},fac[500010],ans;
void init(ll n){
    fac[0]=1;
    for(ll i=1;i<=n;i++){
        fac[i]=fac[i-1]*i%n;
    }
}
ll qmi(ll a,ll k,ll p){
    ll res=1;
    while(k){
        if(k&1) res=res*a%p;
        a=a*a%p;
        k>>=1;
    }
    return res%p;
}
ll C(ll n,ll m,ll p){
    if(n<m) return 0;
    return fac[n]*qmi(fac[n-m],p-2,p)%p*qmi(fac[m],p-2,p)%p;
}
ll lucas(ll n,ll m,ll p){
    if(n<m) return 0;
    if(!n) return 1;
    return lucas(n/p,m/p,p)*C(n%p,m%p,p)%p;
}
void CRT(){
    for(int i=1;i<=4;i++){
        ll Mi=mod/b[i];
        ll t=qmi(Mi,b[i]-2,b[i]);
        ans=(ans+a[i]*Mi%mod*t%mod)%mod;
    }
}
int main(){
    ll n,q;
    cin>>n>>q;
    if(q%(mod+1)==0){
        cout<<0;
        return 0;
    }
    for(int k=1;k<=4;k++){
        init(b[k]);
        for(ll i=1;i*i<=n;i++){
            if(n%i==0){
                a[k]=(a[k]+lucas(n,i,b[k]))%b[k];
                if(i*i!=n){
                    a[k]=(a[k]+lucas(n,n/i,b[k]))%b[k];
                }
            }
        }
    }
    CRT();
    cout<<qmi(q,ans,mod+1);
}

AcWing 212.计数交换(史诗级难度)

首先我们先考虑题目中最少的交换次数m要如何求出。一开始我还在想是不是用归并排序求逆序对的个数,后来才发现我是个**。逆序对的个数表示的其实是:每次交换相邻的两个数最后使得序列有序的最少操作次数。而题目中是可以任意交换的。所以这里要来扩展知识了:如何用最少的操作次数使得1-n的一个任意排列变为升序排列?(一次操作指的是交换序列中的任意两个元素)。我们考虑在p[i]与i之间连一条有向边,容易知道最后这个序列中会存在若干个环。那么我们的目标就是把这若干个环全部变成自环,并求出其最少的操作次数。

我们猜想:把一个长度为n的环变成n个自环,至少需要(n-1)次操作。

证明:利用数学归纳法,当i=1时,结论显然成立。那么假设对于任意整数k(1\leq k \leq n-1),这个结论都成立。那么当k=n时,我们交换这个环中的任意两个元素vi,vj(i<j)。很显然两环的长度分别为j-i与n-(j-i),总操作次数为j-i-1与n-(j-i)-1,总和为n-2,加上之前的操作次数为n-1次。

                                                                                                                                                    证毕

有了这一步的铺垫,下面的转化就更加轻松了。对于上面的证明过程,我们不妨假设将长度为n的环拆成长度分别为x,y的两个环有T(x,y)种方法。那么容易知道:T(x,y)=n/2(if x==y且n为偶数),n(else)。

所以将长度为n的环拆成n个自环的方法总数Fn就出来了:……(注意复习:多重集的排列数:见《算法进阶指南》P171

那么最终答案为:……(式子太复杂,可以自己推,推完了看《算法进阶指南》P173)

当然,这样还不能过掉这一道史诗级的难题。因为Fn的复杂求法,使得时间复杂度达到了O(n^2),但是我们通过找规律发现Fn=n^(n-2),可以把时间复杂度缩减至O(nlogn)。(等我更新后的证明)

总结:我们在多刷题的同时要多记一些可用的模板,作为自己的知识储备。(就像学数学一样,在一道题中接触到的方法也许在以后的某一道题中也可以得到应用)。

错排列

错排列:顾名思义,是指完全错开的排列:即n个信封所匹配的n个信件全部装错的方案总数。那么根据容斥原理,很容易推出下列公式:错排列公式

那么在这里主要讲解一下错排列的递推公式:D[n]=(n-1)(D[n-1]+D[n-2]),证明如下:

考虑第k\left ( 1\leq k< n \right )个数排在第n位,一共有n-1种选法。

再考虑第n个数:

如果第n个数排回了第k位,那么无疑n个位置中已经有2个位置已经确定。那么剩下可能的选择方案数为D[n-2]种。

如果第n个数没有排回第k位,那么此时的n相当于前面过程中任意的k。选择的方式相当于是在剩下的n-1个数中再做一次错排列,方案数为D[n-1]种。

综上所述,D[n]=(n-1)(D[n-1]+D[n-2])

例题1:AcWing 230.排列计数

这题就是一个错排列的裸题了,但是要注意边界情况的特判:

#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e6+5,mod=1e9+7;
typedef long long ll;
ll fac[N],inv[N],d[N];
ll qmi(ll a,ll b,int p){
    ll res=1;
    while(b){
        if(b&1) res=res*a%p;
        a=a*a%p;
        b>>=1;
    }
    return res%p;
}
void init(){
    fac[0]=1;
    for(int i=1;i<N;i++){
        fac[i]=fac[i-1]*i%mod;
        inv[i]=qmi(fac[i],mod-2,mod);
    }
    d[2]=1;
    for(int i=3;i<N;i++){
        d[i]=(i-1)*(d[i-1]+d[i-2])%mod;
    }
}
int main(){
    int T;
    scanf("%d",&T);
    init();
    while(T--){
        ll n,m;
        scanf("%lld%lld",&n,&m);
        if(n-m==1) puts("0");
        else if(n==m) puts("1");
        else if(!m) printf("%lld\n",d[n]);
        else{
            printf("%lld\n",fac[n]*inv[m]%mod*inv[n-m]%mod*d[n-m]%mod);
        }
    }
    return 0;
}

范德蒙德卷积

公式如下:

C_{m+n}^{k}=\sum_{i=0}^{k}C_{m}^{i}C_{n}^{k-i}

这个公式可以用来化简式子,减少时间复杂度。

组合计数类题目的其它解题技巧: 

1.动态规划法

有一些组合计数类题目不易于直接从组合数角度计算,那么不妨从动态规划的角度入手,找出递推关系式来解决问题。甚至组合数的预处理方法也是动态规划法,即:C_{a}^{b}=C_{a-1}^{b-1}+C_{a-1}^{b}

所以引出了求组合数方法1:

#include<iostream>//用杨辉三角来理解也是可以的
using namespace std;
const int N=2005,mod=1e9+7;
int C[N][N];
int main(){
    int n;
    cin>>n;
    for(int i=0;i<N;i++){
        for(int j=0;j<=i;j++){
            if(!j) C[i][j]=1;//三角边界上的1
            else C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;
        }
    }
    while(n--){
        int a,b;
        cin>>a>>b;
        cout<<C[a][b]<<endl;
    }
    return 0;
}

例题1:Bulls and Cows

这题就是一个经典的动态规划解决计数类问题的题目,本题较易,仅供参考。

2.转换角度法

组合计数题有时会存在去重遗漏之类的麻烦情况,那么不妨转换一下看问题的角度,兴许就可以解决问题。

例题1:CQOI2014数三角形

这题本人一开始的想法十分复杂,实际上我们可以直接考虑枚举每一条线段(两个端点可以直接确定,故枚举端点即可)。那么我们选三角形时就不妨直接将这两个端点固定下来,然后在线段上再另找任意一点即可。这样不用考虑一整条所在直线去重之类的情况,不但简化了程序,还做到了不重不漏。

#include<iostream>
using namespace std;
const int N=1e3+5;
typedef long long ll;
int gcd(int a,int b){
    if(!b) return a;
    return gcd(b,a%b);
}
ll C(int a){
    return (ll)a*(a-1)*(a-2)/6;
}
int main(){
    int n,m;
    cin>>n>>m;
    ll res=C((m+1)*(n+1))-(m+1)*C(n+1)-(n+1)*C(m+1);
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            int d=gcd(i,j);
            res-=2ll*(ll)(m-j+1)*(n-i+1)*(d-1);
        }
    }
    cout<<res<<endl;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值