[Java学习] 排列组合相关知识


前言

最近学习了一些排列组合的知识,知识量有些庞杂,因此写一篇博客总结一下。本篇涉及许多数学公式定理,需要联系起来,这样可以方便记忆。


组合数模版

组合数模版1

当询问次数非常多时,即 1 ≤ n ≤ 100000 1\leq n \leq 100000 1n100000 1 ≤ a ≤ b ≤ 2000 1 \leq a \leq b \leq 2000 1ab2000,假设从 a a a 个苹果里选 b b b 个,取其中一个苹果,这个苹果要么在 b b b 里,要么不在 b b b 里,
因此组合数 C a b = C a − 1 b − 1 + C a − 1 b C_a^b=C_{a-1}^{b-1}+C_{a-1}^{b} Cab=Ca1b1+Ca1b

组合数模版2

1 ≤ n ≤ 1 0 4 1\leq n \leq 10^4 1n104
1 ≤ a ≤ b ≤ 1 0 5 1 \leq a \leq b \leq 10^5 1ab105,利用下面公式求解组合数
C a b = a ! b ! × ( a − b ) ! C_a^b=\frac{a!}{{b!}\times(a-b)!} Cab=b!×(ab)!a!

i n f a c t [ b ! ] infact[b!] infact[b!] 表示 b ! b! b! 的逆元,可以利用快速幂求解逆元,因此组合数可以写成:

C a b = a ! b ! × ( a − b ) ! = a ! ∗ i n f a c t [ b ! ] ∗ i n f a c t [ ( a − b ) ! ] C_a^b=\frac{a!}{{b!}\times(a-b)!}=a!*infact[b!]*infact[(a-b)!] Cab=b!×(ab)!a!=a!infact[b!]infact[(ab)!]


快速幂

快速幂是快速求解 a b % p a^b \%p ab%p 的算法,时间复杂度为 O ( n ∗ l o g b ) O(n*logb) O(nlogb)。他的大致思路如下:

  1. 预处理出 a 2 0 , a 2 1 , a 2 2 , … , a 2 l o g b a^{2^{0}},a^{2^1},a^{2^2},\dots,a^{2^{logb}} a20,a21,a22,,a2logb,这几个数。
  2. a b a^b ab a 2 0 , a 2 1 , a 2 2 , … , a 2 l o g b a^{2^{0}},a^{2^1},a^{2^2},\dots,a^{2^{logb}} a20,a21,a22,,a2logb这几个数来组合,即:
    a b = a 2 x 1 × a 2 x 2 × ⋯ × a 2 x t = a 2 x 1 + x 2 + ⋯ + x t a^b=a^{2^{x_1}}\times a^{2^{x_2}}\times \dots\times a^{2^{x_t}}=a^{2^{x_1+x_2+\dots+x_t}} ab=a2x1×a2x2××a2xt=a2x1+x2++xt
    问题:为什么 b b b 可以用 a 2 0 , a 2 1 , a 2 2 , … , a 2 l o g b a^{2^{0}},a^{2^1},a^{2^2},\dots,a^{2^{logb}} a20,a21,a22,,a2logb这几个数来表示?
    因为二进制可以表示任何数,且用单一二进制表示时,b单一表示最大可表示为二进制形式 2 l o g b 2^{logb} 2logb

快速幂的实例代码如下:

import java.io.*;

class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(bufferedReader.readLine());
        for (int i = 0; i < n; i ++) {
            String[] str = bufferedReader.readLine().split(" ");
            int a = Integer.parseInt(str[0]);
            int b = Integer.parseInt(str[1]);
            int p = Integer.parseInt(str[2]);

            int res = qmi(a, b, p);
            System.out.println(res);
        }
    }

    public static int qmi(int a, int b, int p) {
        int res = 1;
        while (b > 0) {
            if (b % 2 == 1) {//判断二进制某一位是否为1
                res = (int)((long)res * a % p);
            }
            b >>= 1;
            a = (int)((long)a * a % p);
        }
        return res;
    }
}



快速幂求逆元

逆元定义

若整数 b , m b, m b,m 互质,并且对于任意整数的 a a a,如果满足 b ∣ a b|a ba,则存在一个数 x x x,使得 a / b ≡ a ∗ x ( m o d    p ) a/b\equiv a*x(mod\; p) a/bax(modp),则称 x x x b b b 的乘法逆元,记为 b − 1 ( m o d    p ) b^{-1}(mod\;p) b1(modp)
b b b 存在逆元的充要条件是 b b b 与模数 m m m 互质。当模数 m m m 为质数时, b m − 2 b^{m-2} bm2 即为 b b b 的乘法逆元。

具体推导

在介绍快速幂求逆元之前,首先了解一下费马小定理。

费马小定理(Fermat’s little theorem)是数论中的一个重要定理,在1636年提出。如果 p p p 是一个质数,而整数
a a a 不是 p p p 的倍数,则有 a p − 1 ≡ 1 ( m o d    p ) a^{p-1}\equiv1(mod\; p) ap11modp)

推导过程如下:
a / b ≡ a ∗ x ( m o d    p ) a/b\equiv a*x(mod\; p) a/bax(modp)
a / b ≡ a ∗ b − 1 ( m o d    p ) a/b\equiv a*b^{-1}(mod\; p) a/bab1(modp)
1 = b − 1 ∗ b ( m o d    p ) 1=b^{-1}*b(mod\;p) 1=b1b(modp)
根据费马小定理: a p − 1 ≡ 1 ( m o d    p ) a^{p-1}\equiv1(mod\; p) ap11modp)
所以: b − 1 ∗ b = b p − 1 = b ∗ b p − 2 b^{-1}*b=b^{p-1}=b*b^{p-2} b1b=bp1=bbp2
因此: b − 1 = b p − 2 b^{-1}=b^{p-2} b1=bp2

结论:当 b b b m m m 互质时, b b b 的乘法逆元为 b − 1 = b p − 2 b^{-1}=b^{p-2} b1=bp2

实现代码如下:

import java.io.*;

class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(bufferedReader.readLine());
        for (int i = 0; i < n; i ++) {
            String[] str = bufferedReader.readLine().split(" ");
            int a = Integer.parseInt(str[0]);
            int p = Integer.parseInt(str[1]);
            if (a % p != 0) {
                System.out.println(qmi(a, p - 2, p));
            } else {
                System.out.println("impossible");
            }
        }
        bufferedReader.close();
    }

    public static int qmi(int a, int b, int p) {
        int res = 1;
        while (b > 0) {
            if (b % 2 == 1) {
                res = (int)((long)res * a % p);
            }
            b >>= 1;
            a = (int)((long)a * a % p);
        }
        return res;
    }
}


求组合数的方法

根据组合数公式: C a b = a ! b ! × ( a − b ) ! C_a^b=\frac{a!}{{b!}\times(a-b)!} Cab=b!×(ab)!a!,结合之前推导出来的快速幂求逆元方法,可以很容易求出,实现的代码如下:

import java.io.*;

class Main {
    private static int N = 100010;
    private static int mod = 1000000007;
    private static int[] fact = new int[N], infact = new int[N];
    
    public static void main(String[] args) throws IOException {
        fact[0] = infact[0] = 1;
        for (int i = 1; i < N; i ++) {
            fact[i] = (int)((long)fact[i - 1] * i % mod);
            infact[i] = (int)((long)infact[i - 1] * qmi(i, mod - 2, mod) % mod);
        }
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(bufferedReader.readLine());
        while (n -- > 0) {
            String[] str = bufferedReader.readLine().split(" ");
            int a = Integer.parseInt(str[0]);
            int b = Integer.parseInt(str[1]);
            System.out.println((int)((long)fact[a] * infact[b] % mod * infact[a - b] % mod));
        }
    }
    
    public static long qmi(int a, int b, int p) {
        long res = 1;
        while(b != 0) {
            if (b % 2 == 1) {
                res = res * a % p;
            }
            a = (int)((long)a * a % p);
            b >>= 1;
        }
        return res;
    }
}


满足条件的01序列

有这样的问题:给定 n n n 0 0 0 n n n 1 1 1,它们将按照某种顺序排成长度为 2 n 2n 2n 的序列,求它们能排列成的所有序列中,能够满足任意前缀序列中 0 0 0 的个数都不少于 1 1 1 的个数的序列有多少个。

输出的答案对 1 0 9 + 7 10^9+7 109+7 取模。

解决该问题的思想如下:
将 01序列置于坐标系中,起点定于原点。若 0 0 0 表示向右走, 1 1 1 表示向上走,那么任何前缀中 0 0 0 的个数不少于 1 1 1 的个数就转化为,路径上的任意一点,横坐标大于等于纵坐标。题目所求即为这样的合法路径数量。

下图中,表示从 ( 0 , 0 ) (0,0) (0,0) 走到 ( n , n ) (n,n) (n,n) 的路径,在绿线及以下表示合法,若触碰红线即不合法。

在这里插入图片描述

这个数字 C 2 n n − C 2 n n − 1 = C 2 n n n + 1 C_{2n}^{n}-C_{2n}^{n-1}=\frac{C_{2n}^n}{n+1} C2nnC2nn1=n+1C2nn 被称为卡特兰数。
由图可知,任何一条不合法的路径(如黑色路径),都对应一条 ( 0 , 0 ) (0,0) (0,0) 走到 ( n − 1 , n + 1 ) (n−1,n+1) (n1,n+1) 的一条路径(如灰色路径)。而任何一条 ( 0 , 0 ) (0,0) (0,0) 走到 ( n − 1 , n + 1 ) (n−1,n+1) (n1,n+1) 的路径,也对应了一条从 ( 0 , 0 ) (0,0) (0,0) 走到 ( n , n ) (n,n) (n,n) 的不合法路径。

答案如图,即卡特兰数。

实现的代码如下:

import java.util.*;

class Main {
    private static int N = 200010, n;
    private static int mod = 1000000007;
    private static int[] fact = new int[N];
    private static int[] infact = new int[N];
    
    public static void main(String[] args) {
        Scanner s = new Scanner(System.in);
        n = s.nextInt();
        fact[0] = infact[0] = 1;
        for (int i = 1; i < N; i ++) {
            fact[i] = (int)((long)fact[i - 1] * i % mod);
            infact[i] = (int)((long)infact[i - 1] * qmi(i, mod - 2, mod) % mod);
        }
        int res = (int)((long)fact[2 * n] * infact[n] % mod * infact[n] % mod * qmi(n + 1, mod - 2, mod) % mod);
        System.out.println(res);
    }
    
    public static int qmi(int a, int b, int p) {
        int res = 1;
        while (b != 0) {
            if (b % 2 == 1) {
                res = (int)((long)res * a % p);
            }
            a = (int)((long)a * a % p);
            b >>= 1;
        }
        return res;
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值