zoukankan      html  css  js  c++  java
  • 组合排列和组合数 学习笔记

      前置知识点

    我们通常使用$P_{n}^{r}$ 表示全排列,而$ CP_{n}^{r}$ 表示圆上全排列, $CC_{n}^{r}$ 表示圆上的组合数

    我们通常使用$inom{n}{r} $ 表示 $C_{n}^{r}$,在下面都会使用 $inom{n}{r} $ 来描述组合数。

    下面给出三种最常见计算组合数计算方法,需要配合逆元食用。

    • 组合数计算公式:$ inom{n}{r} = frac{n!}{r!(n-r)!} $
    • 组合数递推公式:$inom{n}{r} = inom{n-1}{r-1} + inom{n-1}{r}  $
    • 组合数lucas定理:$inom{n}{r}  equiv  inom{left lfloor frac{n}{p} ight floor}{left lfloor frac{r}{p} ight floor} inom{n mod p}{r mod p}  (mod p)  $

    下面是对于组合数应用的一些公式和性质:

    • 对称性:$ inom{n}{r} = inom{n}{n-r} $
    • 二项式定理:$(x+y) ^n = sumlimits_{r=0}^n inom{n}{r}x^{n-r}y^r$
    • 替换定理:$rinom{n}{r} = ninom{n-1}{r-1}$ (常使用在组合数求和式化简)
      • 组合数求和式:$ sumlimits_{r=0} ^n inom{n}{r} = 2^{n} $
      • 等差数列合组合数求和式:$sumlimits_{r=0} ^n r inom{n}{r} = n 2^{n-1}$
      • 等比数列合组合数求和式:$sumlimits_{r=0} ^n r^2 inom{n}{r} = n(n+1) 2^{n-2} $
    • 组合数平方求和式:$sumlimits_{r=0} ^n inom{n}{r}^2 = inom{2n}{n}$

    接下来是一个关于组合数的简单方法——隔板法。

    • 求$x_1 + x_2 +...+x_n = m $正整数解的个数。
      • 考虑到有$m$个小球被$n-1$块板割成不为空的$n$份的方案数。
      • 由于有$m-1$个空,相当于$m-1$个空里插入$n-1$个隔板的方案数。
      • 即$inom{m-1}{n-1}$ 
    • 求$x_1 + x_2 +...+x_n = m $非负整数解的个数。
      • 考虑强制每一个$x_i$初始值为$1$ ,然后再使用上述的正整数解的隔板法。
      • 转化为$m+n$个小球被$n-1$块板割成不为空的$n$份的方案数。
      • 即$inom{n+m-1}{n-1}$

    接下来是一个非常经典的问题:错排列问题

    • 有$n$本书,按照一个原始排列存放,$f(n)$表示重新排列后每一本书都不在自己原来位置的排列总数。
      • 考虑dp,使用$f(i)$表示答案。
      • 将第$i$本书放在$[1,i-1]$的位置$j$上 。对$j$的位置分类:
        • 如果$j$放在$i$的位置上,就是$i-2$个数的错排问题了。
        • 如果$j$不在$i$的位置上,就是$i-1$个数的错排问题了。
      • 于是答案就是: $f(i) = (i-1) (f(i-1)+f(i-2))$

    对于排列数和圆排列,我们可以采用容斥原理,去除一些重复的情况。

    • 排列计算公式:$P_{n}^{r} = frac{n!}{(n-r)!} = n imes (n-1) imes ... imes (n-r+2) imes (n-r+1) $
    • 圆排列计算公式:$CP_{n}^{r} = frac{P_{n}^{r}}{r} = P_{n}^{r-1}$
    • 多重集排列计算公式:$frac{n!}{n_1! n_2! ... n_k!}$,其中$n = n_1 + n_2 +...+n_k$
    • 多重集组合计算公式:含无限重复$k$种元素的集合$S$中选择$k$个元素的组合,等价于选$k$次球,每次都可以选$n$种球的方案数.
      • 同时等价于$k$个相同的球放到$n$个不同的盒子里,盒子可以为空.
      • 就成为了隔板法的经典题。

    接下来是康拓展开和康拓逆展开:

    设$Per(A) = a$ 表示排列A在所有排列中的字典序排名$a$,$acrPer(a) = A$ 表示  字典序排名$a$ 对应的排列A

    显然两个函数为互逆的函数变幻。

    • 求$Per(A) = a$ 康拓展开
      • 设$a_i$ 表示从右往左数第$i$个数右边小于它的数的个数。
      • 那么$a = sumlimits_{i=1}^n a_i imes (i-1)! $
      • 则$a$ 即为所求。
    • 求$acrPer(a) = A$ 康拓逆展开
      • 先将A减去1,表示排列从0开始编号(而事实上排列是以1开始编号),
      • 按照进制转换的方法,按照阶乘求余数,从高到低确定每一位。
      • 对于第$i$ 位上的数,A/(i-1)!  整除的商表示左边有多少个比它小,而余数表示下一次迭代的A值
      • 所以第$i$ 位上的数是 A/(i-1)!  整除的商 + 1

    依靠于组合数几个递推:

    • 第一类斯特林数:$egin{bmatrix} n\  m end{bmatrix}$ 表示$n$个元素进行$m$个非空的圆排列个数。
      • 显然,第$n$个元素可以依靠之前已经完成的圆排列,也可以新成立一个排列。
      • 所以递推式子是:$egin{bmatrix} n\  m end{bmatrix} = (n-1) egin{bmatrix} n-1\  m end{bmatrix} + egin{bmatrix} n-1\  m-1 end{bmatrix}$
      • 边界条件是 : $egin{bmatrix} 0\  m end{bmatrix} = 0 , egin{bmatrix} m\  m end{bmatrix} = 1 $
    • 第二类斯特林数:$egin{Bmatrix} n\m end{Bmatrix}$ 表示$n$个元素划分成$m$个集合的方案数。
      • 显然,第$n$个元素可以依靠之前完成的集合,也可以成立一个新的集合。
      • 所以递推式子是:$egin{Bmatrix} n\m end{Bmatrix} = egin{Bmatrix} n-1\m-1 end{Bmatrix} + m egin{Bmatrix} n-1\m end{Bmatrix}$
      • 边界条件是$egin{Bmatrix} 1\m end{Bmatrix} = egin{Bmatrix} m\m end{Bmatrix} = 1 $

    接下来是一个非常常用的数列,卡特兰数列。

    $Catalan(n) = frac{inom{2n}{n}}{n+1}$ 

    基于一个递推的通项: $Catalan(n) = sumlimits_{i=0} ^ {n-1} Catalan(i) imes Catalan(n-i-1) $

    有下列应用:

    • $n$对括号的合法匹配的方案数(+1/-1 法)         
    • $n$个节点二叉树的形态数
    • $n$个数入栈后出栈的排列总数         
    • 对凸$n+2$边形进行不同的三角形分割的方案数,分割点不能交叉,可以
      在端点处相相交          

      简单例题

      Problem - A   不容易系列之(4)——考新郎

    假设一共有不同的$N$对新婚夫妇,其中有$M$个新郎找错了新娘,求发生这种情况一共有多少种可能。

    对于100%的数据 $n,m leq 20$

      Sol : 首先,从$N$对新婚夫妇中其中$N-M$对都是正确找到了,

    也就是在$M$对新婚夫妇之间的错排问题,而这$M$对是在$N$对之中组合出来的,

    最后答案就是$inom{N}{M} imes f(M)$ 其中$f(M)$表示错排数目。

    # include <bits/stdc++.h>
    # define int long long
    using namespace std;
    const int N=21;
    int f[N];
    int C(int n,int m)
    {
        int s=1;
        for (int i=1;i<=n;i++) s*=i; 
        for (int i=1;i<=m;i++) s/=i;
        for (int i=1;i<=n-m;i++) s/=i; 
        return s;
    }
    signed main()
    {
        f[1]=0; f[2]=1;
        for (int i=3;i<=20;i++) f[i]=(i-1)*(f[i-1]+f[i-2]);
        int T; scanf("%lld",&T);
        while (T--) {
            int n,m; scanf("%lld%lld",&n,&m);
            printf("%lld
    ",C(n,m)*f[m]);
        }
        return 0;
    }
    A.cpp

      Problem - B  Shaass and Lights

     有$n$个路灯,初始情况下已经有$m$个路灯被点亮,接下去每次点灯必须在已经点的灯相邻位置选择一个暗的灯点亮。

      问有多少种点亮剩余所有灯方案% 1e9+7。 

     对于100%的数据$ n,m leq 1000 $

      Sol : 按照点亮的灯对整个序列进行划分块,每一个块当中都是暗的连续的灯。

          每一个非含1,n的块中点灯的顺序,可以从左边或者右边选取,由于最后一个灯只有一种可能所有总可能性是$2^{len-1}$ 

      其中$len$表示块的长度。

      但是,对于每个块同时也可以按照不同的顺序进行选取。这应该是一个组合数的可能性。

      令ret表示剩余灯的个数 ,对于第i个块当中的组合数可能性就是$inom{ret}{len}  $,然后令$ret -= len$即可。

      对于第一个和最后一个(含有1或者n的块)只能从后往前遍历,所以2的幂次的可能性就是1,需要特殊判断。

      预处理组合数之后, 复杂度应该是$O(n^2)$.

    # include <bits/stdc++.h>
    # define int long long
    using namespace std;
    const int mo=1e9+7;
    const int N=1e3+10;
    int n,m,cnt,s[N];
    bool b[N];
    void init(){s[0]=1; for (int i=1;i<=1000;i++) s[i]=s[i-1]*i%mo;}
    int Pow(int x,int n,int p){int ans=1; while (n) { if (n&1) ans=ans*x%mo; x=x*x%mo; n>>=1;} return ans%mo;}
    int inv(int x){return Pow(x,mo-2,mo);}
    int C(int n,int m){return s[n]*inv(s[m])%mo*inv(s[n-m])%mo;}
    signed main()
    {
        init(); scanf("%lld%lld",&n,&m);
        memset(b,false,sizeof(b));
        for (int i=1;i<=m;i++) {int t; scanf("%lld",&t); b[t]=1;    }
        for (int i=1;i<=n;i++) {
            if (b[i]) continue;
            int j=i; while (j<=n&&!b[j]) j++;
            cnt+=j-i; i=j-1;
        }
        int ans=1;
        for (int i=1;i<=n;i++){
            if (b[i]) continue;
            int j=i; while (j<=n&&!b[j]) j++;
            if (i==1) ans=ans*C(cnt,j-i)%mo;
            else if (j-1==n) ans=ans*C(cnt,j-i)%mo;
            else ans=ans*C(cnt,j-i)%mo*Pow(2,j-i-1,mo)%mo;
            cnt-=j-i; i=j-1;
        }
        printf("%lld
    ",ans);
        return 0;
    }
    B.cpp

      Problem - C  Count the Buildings

      有$n$只身高为$[1,n]$牛排队,身高高的牛可以挡住身高矮的牛。

       从前往后看能看到$F$只牛,从后往前看能看到$B$只牛。

       求出有多少种排队方法 % 1e9+7,能够达到上述效果。 

        对于100%的数据$n ,B,Fleq 2000$ 

      Sol :  显然从前面看和从后面看都能看到身高较高的,从n-1只牛里面找出F+B-2个轮换然后挑出较大的来被看,其中F-1组轮换放在左侧,B-1组轮换放在右侧。事实上,这样的方案的前后顺序是固定的,左侧的F-1组轮换能且只能有一种方法排除顺序使得恰好被看F-1只牛(不算最大的),右侧的同理。

       从$n$个数里选择$m$个轮换,需要使用第一类斯特林数求解,而放左侧的$F-1$由于是任意的,所以直接组合数计算即可。

      答案是$ egin{bmatrix}n-1\ F+B-2 end{bmatrix} inom{F+B-2}{F-1} $

      预处理第二类斯特林数后复杂度是 $O(n^2)$ 

    # include <bits/stdc++.h>
    # define int long long
    using namespace std;
    const int N=2e3+10,mo=1e9+7;
    int c[N][N],s[N][N];
    signed main()
    {
        c[0][0]=1;
        for (int i=1;i<=2000;i++) {
            c[i][0]=c[i][i]=1;
            for (int j=1;j<i;j++)
                c[i][j]=(c[i-1][j-1]+c[i-1][j])%mo;
        }
        for (int i=1;i<=2000;i++) {
            s[i][i]=1; s[i][0]=0;
            for (int j=1;j<i;j++)
             s[i][j]=(s[i-1][j-1]+(i-1)*s[i-1][j]%mo)%mo;
        }
        int T; scanf("%lld",&T);
        while (T--) {
            int n,f,b; scanf("%lld%lld%lld",&n,&f,&b);
            int ans;
            if (f+b-2<N) ans=c[f+b-2][f-1]*s[n-1][f+b-2]%mo;
            else ans=0;
            printf("%lld
    ",ans);
        }
        return 0;
    }
    C.cpp

       Problem -D  Examining the Rooms

       每个房间钥匙以某一种排列放在$n$个房间里,每一次可以暴力破拆随机一个房间找出该房间的钥匙,然后进行打开房门-获取钥匙-打开房门...的操作。直到无法再打开新的房门,而进行新的一轮破拆。你的破拆次数最多是$K$次并且$1$号房间的房门不能被破拆,求出你能够成功打开所有门的概率。

     共有$T$组数据 ,对于100%的数据$1 leq kleq n leq 20 ,T leq 100$

       Sol:需要求出$n$把钥匙分成1,2,...k 个非空圆排列的数目$s[n][i] (1 leq i leq k)$。

        还需要考虑第$1$个元素必须要独自成为一个圆排列,所以我们需要把所有合法的答案累加。

        即累加$s[n][i] (1 leq i leq k) - s[n-1][i-1] $ 其中减去的$s[n][i-1]$指的是n个元素形成$i-1$一个环的情况数,此时由于第$1$号房门被暴力破拆于是不合法,所有要减去。

        由于排列数是$n!$所以概率就是$frac{sumlimits_{i=1}^k s[n][i] - s[n][i-1] }{n!}$

    # include <bits/stdc++.h>
    # define int long long
    using namespace std;
    const int N=21;
    int s[N][N];
    signed main()
    {
        for (int i=1;i<=20;i++) {
            s[i][0]=0; s[i][i]=1;
            for (int j=1;j<i;j++)
             s[i][j]=s[i-1][j-1]+(i-1)*s[i-1][j];
        }
        int T; scanf("%lld",&T);
        while (T--) {
            int n,k; scanf("%lld%lld",&n,&k);
            int ret=0;
            for (int i=1;i<=k;i++) ret+=s[n][i]-s[n-1][i-1];
            int s=1;
            for (int i=2;i<=n;i++) s*=i;
            double ans=1.0*ret/(1.0*s);
            printf("%.4lf
    ",ans);
        }
        return 0;
    }
    D.cpp

      

     

    Problem - E Brackets

        有长度为$n$的括号序列,给出前面一部分,前缀和前面部分的括号序列的数量%1e9+7

          对于100%的数据满足$n leq 10^6$ 

      Sol: 对于一个合法的括号序列是满足在任意时刻(数目大于等于)数目并且最终左括号数目和右括号数目相等。

                非常显然,当$n$为奇数的时候答案必然是$0$ ,在前缀括号序列在某一时刻若左括号数目小于右括号数目,答案也必然是$0$

                括号序列其实和网格图记录方案非常相似,可以转化为从(1,1)出发,每读到一个(就往左走一格,每读到一个)就往上走一格,最终走到(n/2,n/2)处

         还有一个限制:不能走到直线y = x 的上方(而在线上可以,满足括号序列合法性)。

         并且由于前缀已经给定所以出发点并不是(1,1),而是(p,q),显然可以计算出。

        为了走到(n/2,n/2)若不考虑限制,则方案数便是$inom{n/2-p+n/2-q}{n/2-q}$

        为了满足上述限制,由于可能是通过$y = x$上方而到达的最终目标,我们把这些不合法方案减去。

        由于坐标都是整点,考虑直线$y = x + 1$及以上不能经过,目标点$(n/2,n/2)$对称过去就是$(n/2-1,n/2+1)$ 

       所有经过$y = x+1$及上方的路线都等价于,从$(p,q)$出发走到$(n/2-1,n/2+1)$的方案,于是我们把这些贡献减去,就是答案。

       最后答案就是$inom{n/2-p+n/2-q}{n/2-q} - inom{n/2-p+n/2-q}{n/2-q + 1} $

    # include <bits/stdc++.h>
    # define int long long
    using namespace std;
    const int N=1e6+10,mo=1e9+7;
    int s[N],inv[N],n;
    char th[N];
    void init()
    {
        s[0]=1; for (int i=1;i<=1000000;i++) s[i]=s[i-1]*i%mo;;
        inv[0]=inv[1]=1;for (int i=2;i<=1000000;i++) inv[i]=(mo-mo/i)%mo*inv[mo%i]%mo;
        for (int i=2;i<=1000000;i++) inv[i]=inv[i-1]*inv[i]%mo;
    }
    int C(int n,int m){return s[n]*inv[m]%mo*inv[n-m]%mo;}
    signed main()
    {
        init();
        while (~scanf("%lld",&n)) {
            scanf("%s",th); int a=0,b=0,ans;
            int len=strlen(th);
            if (n&1) { puts("0"); goto End;}
            for (int i=0;i<len;i++) {
                if (th[i]=='(') a++; else b++;
                if (a<b) { puts("0"); goto End;}
            }
            if (n/2-a<0 || n/2-b<0) { puts("0"); goto End;}
            ans=((C(n/2-a+n/2-b,n/2-b)-C(n/2-a+n/2-b,n/2-b+1))%mo+mo)%mo;
            printf("%lld
    ",ans);
            End:;
        }
        return 0;
    }
    E.cpp

      Problem - F Saving Beans

      $1 ... m$个相同的物品放到$n$个不同箱子里(允许空着不放)共有多少种方法,输出%p的答案。

      共有$T$组数据,对于100%的数据$1 leq n,m leq 10^9 , 1 leq p leq 10^5$

      Sol:考虑把$k$个相同$n$个不同箱子里(允许空着不放)共有多少种方法。

        等价于求$x_1 + x_2 + ... + x_k = n$的非负整数解个数,就是$inom{n+k-1}{k-1}$

        所以答案对于$k in [1,m]$ 求和, 等价于求$sumlimits _{k=1} ^ {m}inom{n+k-1}{k-1} = inom{n}{0} + inom{n+1}{1} + ... + inom{n+m-1}{m-1} $ 

        第一项$ inom{n}{0}  =  inom{n+1}{0}  $将其替换。

        由组合数递推公式$inom{n}{r} = inom{n-1}{r-1} + inom{n-1}{r}$ 可将相邻两项分别合并。

        得$ inom{n+1}{0} + inom{n+1}{1} + ... + inom{n+m-1}{m-1} = inom{n+m}{m}$

        直接套用lucas定理即可。

    # include <bits/stdc++.h>
    # define int long long
    using namespace std;
    const int N=2e5+10;
    int s[N],inv[N],n,m,p;
    void init(int len)
    {
        s[0]=inv[0]=inv[1]=1;
        for (int i=1;i<=len;i++) s[i]=s[i-1]*i%p;
        for (int i=2;i<=len;i++) inv[i]=(p-p/i)%p*inv[p%i]%p;
        for (int i=2;i<=len;i++) inv[i]=inv[i-1]*inv[i]%p;
    }
    int lucas(int n,int m,int p)
    {
        if (m>n) return 0;
        if (n<p&&m<p) return s[n]*inv[n-m]%p*inv[m]%p;
        return lucas(n%p,m%p,p)*lucas(n/p,m/p,p)%p;
    }
    signed main()
    {
        int T;scanf("%lld",&T);
        while (T--) {
            scanf("%lld%lld%lld",&n,&m,&p);
            init(n+m);
            printf("%lld
    ",lucas(n+m,m,p));
        }
        return 0;
    }
    F.cpp
  • 相关阅读:
    错误处理和调试 C++快速入门30
    错误处理和调试 C++快速入门30
    虚继承 C++快速入门29
    多继承 C++快速入门28
    界面设计01 零基础入门学习Delphi42
    鱼C记事本 Delphi经典案例讲解
    界面设计01 零基础入门学习Delphi42
    虚继承 C++快速入门29
    linux系统中iptables防火墙管理工具
    linux系统中逻辑卷快照
  • 原文地址:https://www.cnblogs.com/ljc20020730/p/11302718.html
Copyright © 2011-2022 走看看