前言
最近学习了一些排列组合的知识,知识量有些庞杂,因此写一篇博客总结一下。本篇涉及许多数学公式定理,需要联系起来,这样可以方便记忆。
组合数模版
组合数模版1
当询问次数非常多时,即
1
≤
n
≤
100000
1\leq n \leq 100000
1≤n≤100000,
1
≤
a
≤
b
≤
2000
1 \leq a \leq b \leq 2000
1≤a≤b≤2000,假设从
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=Ca−1b−1+Ca−1b
组合数模版2
1
≤
n
≤
1
0
4
1\leq n \leq 10^4
1≤n≤104
1
≤
a
≤
b
≤
1
0
5
1 \leq a \leq b \leq 10^5
1≤a≤b≤105,利用下面公式求解组合数
C
a
b
=
a
!
b
!
×
(
a
−
b
)
!
C_a^b=\frac{a!}{{b!}\times(a-b)!}
Cab=b!×(a−b)!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!×(a−b)!a!=a!∗infact[b!]∗infact[(a−b)!]
快速幂
快速幂是快速求解 a b % p a^b \%p ab%p 的算法,时间复杂度为 O ( n ∗ l o g b ) O(n*logb) O(n∗logb)。他的大致思路如下:
- 预处理出 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^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
b∣a,则存在一个数
x
x
x,使得
a
/
b
≡
a
∗
x
(
m
o
d
p
)
a/b\equiv a*x(mod\; p)
a/b≡a∗x(modp),则称
x
x
x 为
b
b
b 的乘法逆元,记为
b
−
1
(
m
o
d
p
)
b^{-1}(mod\;p)
b−1(modp)
b
b
b 存在逆元的充要条件是
b
b
b 与模数
m
m
m 互质。当模数
m
m
m 为质数时,
b
m
−
2
b^{m-2}
bm−2 即为
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) ap−1≡1(modp)。
推导过程如下:
a
/
b
≡
a
∗
x
(
m
o
d
p
)
a/b\equiv a*x(mod\; p)
a/b≡a∗x(modp)
a
/
b
≡
a
∗
b
−
1
(
m
o
d
p
)
a/b\equiv a*b^{-1}(mod\; p)
a/b≡a∗b−1(modp)
1
=
b
−
1
∗
b
(
m
o
d
p
)
1=b^{-1}*b(mod\;p)
1=b−1∗b(modp)
根据费马小定理:
a
p
−
1
≡
1
(
m
o
d
p
)
a^{p-1}\equiv1(mod\; p)
ap−1≡1(modp)
所以:
b
−
1
∗
b
=
b
p
−
1
=
b
∗
b
p
−
2
b^{-1}*b=b^{p-1}=b*b^{p-2}
b−1∗b=bp−1=b∗bp−2
因此:
b
−
1
=
b
p
−
2
b^{-1}=b^{p-2}
b−1=bp−2
结论:当 b b b 与 m m m 互质时, b b b 的乘法逆元为 b − 1 = b p − 2 b^{-1}=b^{p-2} b−1=bp−2
实现代码如下:
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!×(a−b)!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}
C2nn−C2nn−1=n+1C2nn 被称为卡特兰数。
由图可知,任何一条不合法的路径(如黑色路径),都对应一条
(
0
,
0
)
(0,0)
(0,0) 走到
(
n
−
1
,
n
+
1
)
(n−1,n+1)
(n−1,n+1) 的一条路径(如灰色路径)。而任何一条
(
0
,
0
)
(0,0)
(0,0) 走到
(
n
−
1
,
n
+
1
)
(n−1,n+1)
(n−1,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;
}
}