组合排列和组合数 学习笔记

  前置知识点

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

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

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

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

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

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

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

  • 求$x_1 + x_2 +...+x_n = m $正整数解的个数。
    • 考虑到有$m$个小球被$n-1$块板割成不为空的$n$份的方案数。
    • 由于有$m-1$个空,相当于$m-1$个空里插入$n-1$个隔板的方案数。
    • 即$\binom{m-1}{n-1}$ 
  • 求$x_1 + x_2 +...+x_n = m $非负整数解的个数。
    • 考虑强制每一个$x_i$初始值为$1$ ,然后再使用上述的正整数解的隔板法。
    • 转化为$m+n$个小球被$n-1$块板割成不为空的$n$份的方案数。
    • 即$\binom{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 \times (n-1) \times ... \times (n-r+2) \times (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 = \sum\limits_{i=1}^n a_i \times (i-1)! $
    • 则$a$ 即为所求。
  • 求$acrPer(a) = A$ 康拓逆展开
    • 先将A减去1,表示排列从0开始编号(而事实上排列是以1开始编号),
    • 按照进制转换的方法,按照阶乘求余数,从高到低确定每一位。
    • 对于第$i$ 位上的数,A/(i-1)!  整除的商表示左边有多少个比它小,而余数表示下一次迭代的A值
    • 所以第$i$ 位上的数是 A/(i-1)!  整除的商 + 1

依靠于组合数几个递推:

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

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

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

基于一个递推的通项: $Catalan(n) = \sum\limits_{i=0} ^ {n-1} Catalan(i) \times 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$对之中组合出来的,

最后答案就是$\binom{N}{M} \times 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\n",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个块当中的组合数可能性就是$\binom{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\n",ans);
    return 0;
}
B.cpp

  Problem - C  Count the Buildings

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

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

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

    对于100%的数据$n ,B,F\leq 2000$ 

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

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

  答案是$ \begin{bmatrix}n-1\\ F+B-2 \end{bmatrix} \binom{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\n",ans);
    }
    return 0;
}
C.cpp

   Problem -D  Examining the Rooms

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

 共有$T$组数据 ,对于100%的数据$1 \leq k\leq 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{\sum\limits_{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\n",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)若不考虑限制,则方案数便是$\binom{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)$的方案,于是我们把这些贡献减去,就是答案。

   最后答案就是$\binom{n/2-p+n/2-q}{n/2-q} - \binom{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\n",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$的非负整数解个数,就是$\binom{n+k-1}{k-1}$

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

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

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

    得$ \binom{n+1}{0} + \binom{n+1}{1} + ... + \binom{n+m-1}{m-1} = \binom{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\n",lucas(n+m,m,p));
    }
    return 0;
}
F.cpp

 

posted @ 2019-08-05 14:17  ljc20020730  阅读(665)  评论(0编辑  收藏  举报