组合数学
排列
A a b = a ! ( a − b ) ! A_a^b = \frac{a!}{(a - b)!} Aab=(a−b)!a!
可重复排列数
从 n 个物品可重复的取k个排列数为 n k n^k nk
错排问题
n封不同的信, 编号分别是 1, 2, 3, 4, 5 现在要把五封信放在编号1, 2, 3, 4, 5的信封中, 要求信封的编号和信编号不同, 有多少种不同的放置方法
n个人将各自的帽子混在一起后任取一项,求恰有k个人拿对自己的帽子的概率:
错排问题的定义如下:
给定n元素集合X,它的每一个元素都有一个特定的位置,而现在要求求出集合X的排列中没有一个元素在它指定位置上的排列的数目.
用 D n 表 示 1 , 2 , . . . , n 错 排 数 目 , 根 据 容 斥 原 理 可 以 得 到 D n 的 公 式 ( 证 明 见 《 组 合 数 学 》 第 五 版 108 页 ) 用D_n表示{1,2,...,n} 错排数目,根据容斥原理可以得到D_n的公式(证明见《组合数学》第五版108页) 用Dn表示1,2,...,n错排数目,根据容斥原理可以得到Dn的公式(证明见《组合数学》第五版108页)
D n = n ! ( 1 − 1 / 1 ! + 1 / 2 ! − 1 / 3 ! + . . . + ( − 1 ) n ∗ 1 / n ! ) D_n = n!(1-1/1!+1/2!-1/3!+...+(-1)^n*1/n!) Dn=n!(1−1/1!+1/2!−1/3!+...+(−1)n∗1/n!)
还可以通过
D
1
=
0
,
D
2
=
1
D_1=0, D_2=1
D1=0,D2=1
出发,递推得:
D n = ( n − 1 ) ( D n − 1 + D n − 2 ) ( D 1 = 0 , D 2 = 1 ) D_n = (n - 1)(D_{n-1} + D_{n - 2})\quad(D_1 = 0, D_2 = 1) Dn=(n−1)(Dn−1+Dn−2)(D1=0,D2=1)
D n − m ∗ C n m 为 答 案 D_{n - m} * C_n^m 为答案 Dn−m∗Cnm为答案
不相邻排列
n 个自然数中选 k 个, 这 k 个数中任何两个数都不相邻的组合有 C n − k + 1 k C_{n-k+1}^k Cn−k+1k 种
圆排列
有五个小朋友,手拉手排成一个圆做游戏,求不同的排法数?
n个人围成一圈, 所有排列数记为 Q n n Q_n^n Qnn , 考虑其中已经排好的一圈,从不同位置断开,又变成不同的队列。 所以有 Q n n ∗ n = A n n = ( n − 1 ) ! Q^n_n * n = A_n^n = (n - 1)! Qnn∗n=Ann=(n−1)!
可知 n 个不同物品中取 m 个的圆排列数公式:
Q
n
r
=
A
n
r
/
r
=
n
!
/
(
r
∗
(
n
−
r
)
!
)
Q_n^r=A_n^r/r = n!/(r*(n-r)!)
Qnr=Anr/r=n!/(r∗(n−r)!)
不尽相异元素全排列
如果 n 个元素里, 有 p 个元素相同, 又有 q 个元素相同…,又有r个元素相同 (p+q+… +r ≤n), 则它的所有排列种数为:
n
!
p
!
q
!
.
.
.
r
!
\frac{n!}{p!q!...r!}
p!q!...r!n!
多重集全排列
同理如果 a1 重复 n1次, a2 重复 n2次, …, ak 重复 nk 次, 全排列数为 n ! n 1 ! n 2 ! . . . n k ! \frac{n!}{n1!n2!...nk!} n1!n2!...nk!n! (n = n1 + n2 + … + nk)
组合
C a b = a ! b ! ( a − b ) ! C_a^b = \frac{a!}{b!(a-b)!} Cab=b!(a−b)!a!
递推求组合数:
C
a
b
=
C
a
−
1
b
+
C
a
−
1
b
−
1
C_a^b = C_{a-1}^b + C_{a-1}^{b-1}
Cab=Ca−1b+Ca−1b−1
当 a 和 b 不大的时候可以预处理, 复杂度
O
(
n
2
)
O(n^2)
O(n2) 对 mod 取余
const int N =2010, mod = 1e9 + 7;
int c[N][N];
void init() // 预处理函数
{
for(int i = 0; i < N; i++) // 注意是从0开始
for(int j = 0;j <= i; j++)
if(!j) c[i][j] = 1; // b = 0,组合数定义为1
else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
}
当 a 和 b 超过 1 0 4 10^4 104 则不能用递推预处理
显然对于乘法来说有了同余定理, 但是对除法没有, 所以用快速幂求逆元, 复杂度 O ( n l o g n ) O(nlog_n) O(nlogn)
fact[0] = infact[0] = 1;//注意初始化
for(int i = 1; i < N; i ++)
fact[i] = (LL)fact[i - 1] * i % mod;//注意这里和下面的强制long long 转化。不然会爆int
for(int i = 1; i < N ; i++)
infact[i] = (LL)infact[i - 1] * ksm(i, mod - 2, mod) % mod;//快速幂求逆元
当 a 和 b 为longlong 类型 一般用卢卡斯定理求解
C
a
b
=
C
a
%
p
b
%
p
∗
C
a
/
p
b
/
p
(
m
o
d
p
)
C_a^b = C_{a \% p}^{b\%p} * C_{a/p}^{b/p}(mod p)
Cab=Ca%pb%p∗Ca/pb/p(modp)
int C(int a,int b) {
int res = 1;
for(int i = a, j = 1; j <= b; j++, i--) {
res = (LL)res * i % p;
res = (LL)res * ksm(j, p - 2) % p;
}
return res;
}
LL lucas(LL a, LL b) {
if(a < p && b < p ) return C(a, b);
return (LL)C(a % p , b % p ) * lucas(a / p, b / p) % p; //及时mod,LL
}
组合数常用公式
- C n m = C n − 1 m + C n − 1 m − 1 C_n^m = C_{n-1}^m + C_{n-1}^{m-1} Cnm=Cn−1m+Cn−1m−1
- C n m = C n n − m C_n^m = C_n^{n-m} Cnm=Cnn−m
- C n m + 1 = ( n − m ) / ( m + 1 ) ∗ C n m C_n^{m+1} = (n-m)/(m+1) * C_n^m Cnm+1=(n−m)/(m+1)∗Cnm
可重组合数
可重组合是一类组合,从非空集合 X={1,2,…,n} 中,每次取出 r 个元素,允许元素重复,且不计顺序,这种组合称为集合 X 的一个 r 可重组合,集合 X 的 r 可重组合的总数为
H
n
r
=
C
n
+
r
−
1
r
=
(
n
+
r
−
1
)
!
r
!
(
n
−
1
)
!
H_n^r=C_{n+r-1}^{r}=\frac{(n+r-1)!}{r!(n-1)!}
Hnr=Cn+r−1r=r!(n−1)!(n+r−1)!
模型:
取 r 个无标志的球,n 个有区别的盒子,每个盒子允许放多于一个球,或者空盒
不相邻组合
不相邻的组合是指从 A = {1, 2, … n} 中取 r 个不相邻的数进行组合(不可重) 组合数为:
C
n
−
r
+
1
r
C_{n-r+1}^{r}
Cn−r+1r
模型:
某保密装置须同时使用若干把不同的钥匙才能打开. 现有7人, 每人持若干钥匙. 须4人到场, 所备钥匙才能开锁
问1: 至少有多少把不同的锁?
答: 每三个人至少缺一把钥匙, 任意四个人不缺钥匙, 那么每三个人所缺的钥匙必须不同, 如果有两组缺的钥匙相同(abc 缺 x, abd 缺 x), 那么 abcd 还是缺 x, 因此答案为 C 7 3 C_7^3 C73
问2: 每人至少持几把钥匙?
答: 任意4个人都不缺钥匙, 任一人对于其他6人中的每3人, 都至少有1把钥匙这三人不同才能开锁。
所以每人至少
C
6
3
C_6^3
C63
字典序
839647521 的下一个字典序
从后往前找,第一个下降的的数是 4,与从后往前第一个大于它的数 5 交换变成 839657421,然后将 5 之后的数按照递增排列 (其实就是一个翻转) 即 839651247
代码
public static String nxtPermutation(String nums) {
int len = nums.length();
int first = 0, second = 0;
boolean f = false;
for(int i = len - 2; i >= 0; i--) {
if(nums.charAt(i) < nums.charAt(i + 1)) {
first = i;
f = true;
break;
}
}
// 如果是降序排列
if(!f) {
return null;
}
for(int i = len - 1; i >= 0; i--) {
if(nums.charAt(i) > nums.charAt(first)) {
second = i;
break;
}
}
StringBuffer sb = new StringBuffer(nums);
char tmp = sb.charAt(first);
sb.setCharAt(first, sb.charAt(second));
sb.setCharAt(second, tmp);
for(int i = first + 1, j = len - 1; i <= j; i++, j--) {
tmp = sb.charAt(i);
sb.setCharAt(i, sb.charAt(j));
sb.setCharAt(j, tmp);
}
return sb.toString();
}
找出字符串的最大字典序的子序列
最后一个字符肯定是最大子序列中的一个字符,然后假定最大的字符为 ‘a’,从字符串最后一个字符开始变量,若该字符大于或等于最大字符,则让该字符入栈,并且将该字符替换成最大的字符
找出字符串的最大字典序的子串
先找到最大字典序的字符, 然后用双指针标识两个最大的字符, 然后开始以两个指针为起点遍历, 如果遇到不同的判断大小, 满足条件就改变 begin 指针的位置, 最后得到的 begin 到 len 的字串就是所求
public static String getMaxSubString(String s) {
int len = s.length();
int bg = 0;
for(int i = 1; i < len; i++) {
if(s.charAt(i) > s.charAt(bg)) {
bg = i;
}
}
for(int i = bg + 1; i < len; i++) {
if(s.charAt(i) == s.charAt(bg)) {
for(int j = bg, k = i; j < len && k < len; j++, k++) {
if(s.charAt(j) != s.charAt(k)) {
if(s.charAt(j) < s.charAt(k)) {
bg = i;
}
break;
}
}
}
}
return s.substring(bg, len);
}
卡特兰数
对卡特兰数的初步理解:有一些操作,这些操作有着一定的限制,如一种操作数不能超过另外一种操作数,或者两种操作不能有交集等,这些操作的合法操作顺序的数量
卡特兰数(fn) 的前几项为:
1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862, 16796, 58786, 208012, 742900, 2674440, 9694845, 35357670, 129644790…
f
n
=
f
0
∗
f
n
−
1
+
f
1
∗
f
n
−
2
+
.
.
.
f
n
−
1
∗
f
0
(
n
>
=
2
)
f_n = f_0 * f_{n-1} + f_1 * f_{n-2} + ... f_{n-1} * f_0 (n >= 2)
fn=f0∗fn−1+f1∗fn−2+...fn−1∗f0(n>=2)
递推关系:
f
n
=
4
n
−
2
n
+
1
f
n
−
1
f_n = \frac{4n-2}{n+1}f_{n-1}
fn=n+14n−2fn−1
通项公式:
f
n
=
1
n
+
1
C
2
n
n
f_n=\frac{1}{n+1}C_{2n}^n
fn=n+11C2nn
经过化简后得:
f
n
=
C
2
n
n
−
C
2
n
n
−
1
f_n = C_{2n}^n - C_{2n}^{n-1}
fn=C2nn−C2nn−1
只要我们在解决问题时得到了上面的一个关系,那么你就已经解决了这个问题,因为他们都是卡特兰数列
实际问题
-
n 个元素的二叉查找树有多少种
-
n * n棋盘从左下角走到右上角而不穿过主对角线的走法
-
2n 个人排队买票问题,票价 50,n 个人拿 50 元,n 个人拿 100元,售票处无零钱,能顺利卖票的所有排队方式
-
n 个元素全部入栈并且全部出栈的所有可能顺序
-
你现在有 n 个 0 和 n 个 1 ,问有多少个长度为 2n 的序列,使得序列的任意一个前缀中 1 的个数都大于等于 0 的个数
-
你有 n 个左括号, n 个右括号,问有多少个长度为 2n 的括号序列使得所有的括号都是合法的
-
有一个栈,我们有 2n 次操作, n 次进栈, n 次出栈,问有多少中合法的进出栈序列
这些问题的答案都是卡特兰数F(n)。但是很明显可以看出后三个问题是同质的
都可以抽象成 2n 个操作组成的操作链,其中A操作和B操作各 n 个,且要求截断到操作链的任何位置都有:A操作(向右走一步、收到50元、元素入栈)的个数不少于B操作(向上走一步、收到100元找出50元、元素出栈)的个数
n <= 35 的卡特兰数
public static long f[] = new long[36];
public static void getCatalan() {
f[0] = f[1] = 1L;
for(int i = 2; i < 36; i++) {
f[i] = 0;
for(int j = 0; j < i; j++) {
f[i] = f[i] + f[j] * f[i - j + 1];
}
}
}
n >= 35 的卡特兰数通过卢卡斯定理以及求解组合数
int C(int a,int b) {
int res = 1;
for(int i = a, j = 1; j <= b; j++, i--) {
res = (LL)res * i % p;
res = (LL)res * ksm(j, p - 2) % p;
}
return res;
}
LL lucas(LL a, LL b) {
if(a < p && b < p ) return C(a, b);
return (LL)C(a % p , b % p ) * lucas(a / p, b / p) % p; //及时mod,LL
}
容斥原理
计算几个集合并集的大小,先计算出所有单个集合的大小,减去所有两个集合相交的部分,加上三个集合相交的部分,再减去四个集合相交的部分,以此类推,一直计算到所有集合相交的部分 。
主要是用于求解逆问题
题意:给定整数n和r,求区间[1, r]中与n互素的数的个数
先求1 ~ r 中和m 不互质的个数
那么这个时候我们就需要筛m 的质因数,然后求1 ~ r 中和 m 的质因数能被整除的个数,但是有些数会被筛多次,所以我们需要求并集,然后减去这个并集,利用容斥原理,先求不互质的个数 ans,最后结果 n−ans。
先将n分解质因子。存到v 容器里。
假如n 有 2,3,5质因子,那么2, 3, 5的倍数与m 都不互质,但是会有重复。用容斥原理算出正确的即可。
r / 2 + r / 3 + r / 5 - r / (2 * 3) -r / (3 * 5) -r / (2 * 5) + r / (2 * 3 * 5)
出现奇数次的加,出现偶数次的减。
代码枚举所有质因子的组合时用二进制枚举。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll ans;
int slove(int n, int r) {
vector<int> v;
for(int i = 2; i * i <= n; i++) { // 筛互质数
if(n % i == 0) {
v.push_back(i);
while(n % i == 0) {
n /= i;
}
}
}
if(n > 1) v.push_back(n); //这时候 n就也是一个质数,
ll tmp, s = 0;
for(int i = 1; i < (1 << v.size()); i++) {
tmp = 1, s = 0;
for(int k = 0; k < v.size(); k++) {
if(i & (1 << k)) {
tmp *= v[k];
s++;
}
}
if(s & 1) {
ans += (ll)r / tmp;
} else {
ans -= (ll)r / tmp;
}
}
return r - ans;
}
int main() {
ios::sync_with_stdio(false);
int n, r;
cin >> n >> r;
cout << slove(n, r);
return 0;
}
题意:给出n个互质数ai和整数r。求在区间[1;r]中,至少能被一个ai整除的数有多少。
解决此题的思路和上题差不多,计算ai所能组成的各种集合(这里将集合中ai的最小公倍数作为除数)在区间中满足的数的个数,然后利用容斥原理实现加减。
此题中实现所有集合的枚举,需要2^n的复杂度,求解lcm需要O(nlogr)的复杂度。
如果是n 个整数的话就需要去掉非质数
#include<bits/stdc++.h>
#define ll long long
using namespace std;
int a[10000000], ans, tmp;
int solve(int n, int r) {
for(int i = 1; i < (1 << n); i++) {
tmp = 1; int count = 0;
for(int k = 0; k < n; k++) {
if(i & (1 << k)) {
tmp *= a[k];
count++;
}
}
ans += count & 1 ? (r / tmp) : -(r / tmp);
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
int n, r;
cin >> n >> r;
for(int i = 0; i < n; i++) {
cin >> a[i];
}
cout << solve(n, r) << endl;
return 0;
}
二项式定理
公式:
(
x
+
y
)
n
=
∑
i
=
0
n
C
n
i
x
i
y
n
−
i
(x + y)^n = \sum_{i=0}^{n}C_n^i{x^iy^{n-i}}
(x+y)n=i=0∑nCnixiyn−i
例题: 你有一枚硬币,然后抛出,落在地上,正面朝上的概率是p/q。现在你扔k次,问你正面朝上的次数是偶数次的概率
很容易想到的是分母为 p k p^k pk 分子为 ∑ i = 0 k C k i ∗ q i ∗ ( p − q ) k − i ( i 为 偶 数 ) \sum_{i=0}^{k}C_k^i*q^i*(p-q)^{k-i}(i为偶数) ∑i=0kCki∗qi∗(p−q)k−i(i为偶数)
对应二项式定理, q 为 x, (p - q) 为 y
根据二项式偶数项相加 = ((x + y) ^ k + (x - y) ^k) / 2
所以最后快速幂 + 逆元解决, 其实对 / 2 处理一下就用不到逆元了 (
母函数
把组合问题的加法法则和幂级数的的乘幂的相加对应起来
母函数的思想很简单—就是把离散数列和幂级数一一对应起来,把离散数列间的相互结合关系对应成为幂级数间的运算关系,最后由幂级数形式来确定离散数列的构造
有1克、2克、3克、4克的砝码各一枚,能称出哪几种重量?每种重量各有几种可能方案?
那么接下来试试用这种表达式表示4个砝码的组合情况
(x^0 + x^1)* (x^0 + x^2) * (x^0 + x^3)* (x^0 + x^4)
= x^0 + x^1 + x^2 + 2x^3 + 2x^4 + 2x^5 + 2x^6 + 2x^7 + x^8+ x^9 + x^10
求用1g、2g、3g的砝码称出不同重量的方案数
大家把这种情况和第一种比较有何区别?第一种每种是一个,而这里每种是无限的。
怎样在母函数里表现出无限这种性质呢?很简单,我们以 2g 砝码为例,因为我们有无限个 2g 砝码,所以我们可以把 2 个 2g 砝码看成是 4g 砝码,3 个 2g 砝码看成是 6g 砝码,依次类推,以前两个砝码为例,那么多项式相应的就变成
(x^0 + x^1 + x^2 + x^3 + x^4 + x^5 + …… ) * (x^0 + x^2 + x^4 + x^6 + x^8+ x^10 + ……) * (x^0 + x^3 + x^6)
无限砝码情况下的某种重量的方案数 (和上面的样例不同, 这里每一种克都有且无限)
public class Main {
public static final int N = 1000;
public static int[] sup = new int[N];
public static int[] tmp = new int[N];
public static void main(String[] args) {
InputReader in = new InputReader();
int target = 0;
while(in.hasNext()) {
target = Integer.parseInt(in.next());
// 初始化
// 此时 sup 是用 1g 砝码的多项式, 如果没给 1g 的砝码就不是 i++
// 而是 i += 砝码的重量
Arrays.fill(sup, 1);
Arrays.fill(tmp, 0);
// 生成后续的第 i 个多项式, 从 2g 的砝码开始
for(int i = 2; i <= target; i++) {
// 遍历当前结果的第 j 项与第 i 个相乘
for(int j = 0; j <= target; j++) {
for(int k = 0; k + j <= target; k += i) {
// 幂运算
tmp[j + k] += sup[j];
}
}
for(int j = 0; j <= target; j++) {
// 临时结果覆盖当前结果, 同时初始化 tmp
sup[j] = tmp[j];
tmp[j] = 0;
}
}
System.out.println(sup[target]);
}
}
static class InputReader {
StringTokenizer tok = null;
BufferedReader br = null;
InputReader() {
br = new BufferedReader(new InputStreamReader(System.in));
}
public boolean hasNext() {
while(tok == null || !tok.hasMoreElements()) {
try {
tok = new StringTokenizer(br.readLine());
} catch (Exception e) {
return false;
}
}
return true;
}
public String next() {
return hasNext() ? tok.nextToken() : null;
}
}
}
二维母函数模板
1, 5, 10, 25, 50 五种硬币, 和一个价值 n, 问由硬币总数不超过 100 的方案有多少种
只需要在传统的的母函数上加上一维,来对数量加以限定
public class Main {
public static final int N = 1000;
public static int[][] sup = new int[N][N]; // 二维来处理数量
public static int[][] tmp = new int[N][N];
public static int[] num = new int[]{0, 1, 5, 10, 25, 50};
public static int[] count = new int[6];
public static void main(String[] args) {
InputReader in = new InputReader();
int target = 0;
while(in.hasNext()) {
target = Integer.parseInt(in.next());
Arrays.fill(sup, 0);
Arrays.fill(tmp, 0);
// 0 也是一种方案
if (target == 0) {
System.out.println(1);
continue;
}
for(int i = 0; i <= 100; i++) {
sup[i][i] = 1;
}
// 每种金额的最大数量
for(int i = 1; i <= 5; i++) {
count[i] = target / num[i];
}
for(int i = 1; i <= 5; i++) {
for(int j = 0; j <= target; j++) {
for(int k = 0; j + k <= target && k <= num[i] * count[i]; k += num[i]) {
// 处理数量
for(int l = 0; l + k / num[i] <= 100; l++) {
tmp[j + k][l + k / num[i]] += sup[j][l];
}
}
}
for(int j = 0; j <= target; j++) {
for(int l = 0; l <= 100; l++) {
sup[j][l] = tmp[j][l];
tmp[j][l] = 0;
}
}
}
int ans = 0;
for(int i = 0; i <= 100; i++) {
ans += sup[target][i];
}
System.out.println(ans);
}
}
static class InputReader {
StringTokenizer tok = null;
BufferedReader br = null;
InputReader() {
br = new BufferedReader(new InputStreamReader(System.in));
}
public boolean hasNext() {
while(tok == null || !tok.hasMoreElements()) {
try {
tok = new StringTokenizer(br.readLine());
} catch (Exception e) {
return false;
}
}
return true;
}
public String next() {
return hasNext() ? tok.nextToken() : null;
}
}
}
指数型母函数
用于求解多重集的排列问题
n个元素,其中a1, a2, ……, an互不相同,进行全排列,可得 n! 个不同的排列
若其中某一元素 ai 重复了 ni 次, 全排列出来必有重复元素,其中真正不同的排列数应为 n ! n i ! \frac{n!}{ni!} ni!n!, 重复度为 ni!
同理如果 a1 重复 n1次, a2 重复 n2次, …, ak 重复 nk 次, 全排列数为 n ! n 1 ! n 2 ! . . . n k ! \frac{n!}{n1!n2!...nk!} n1!n2!...nk!n!
若只对其中的 r 个元素进行排列, 就用到了指数型母函数
G e ( x ) = a 0 + a 1 1 ! x + a 2 2 ! x 2 . . . G_e(x)=a_0 + \frac{a_1}{1!}x + \frac{a_2}{2!}x^2 ... Ge(x)=a0+1!a1x+2!a2x2...
物品 n 种, 每种数量分别为 k1, k2, …kn个, 求从中选出 m 个物品的排列方法数
构造母函数:
因为对于第一个元素可取的个数的范围是{0, 1, 2, 3…k1} 所以其指数型生成函数为 ( 1 + x 1 ! + x 2 2 ! . . . x k 1 k 1 ! ) (1+\frac{x}{1!}+\frac{x^2}{2!}...\frac{x^{k1}}{k1!}) (1+1!x+2!x2...k1!xk1)
每一个元素的通项: x k / k ! x^k / k! xk/k!
同理:
G
(
x
)
=
(
1
+
x
1
!
+
x
2
2
!
.
.
.
x
k
1
k
1
!
)
(
1
+
x
1
!
+
x
2
2
!
.
.
.
x
k
2
k
2
!
)
.
.
.
(
1
+
x
1
!
+
x
2
2
!
.
.
.
x
k
n
k
n
!
)
=
a
0
+
a
1
x
+
a
2
2
!
x
2
.
.
.
a
p
p
!
x
p
(
p
=
k
1
+
k
2..
k
n
)
G(x)=(1+\frac{x}{1!}+\frac{x^2}{2!}...\frac{x^{k1}}{k1!})(1+\frac{x}{1!}+\frac{x^2}{2!}...\frac{x^{k2}}{k2!})...(1+\frac{x}{1!}+\frac{x^2}{2!}...\frac{x^{kn}}{kn!}) =a_0 + a_1x + \frac{a_2}{2!}x^2 ...\frac{ap}{p!}x^p (p=k1+k2..kn)
G(x)=(1+1!x+2!x2...k1!xk1)(1+1!x+2!x2...k2!xk2)...(1+1!x+2!x2...kn!xkn)=a0+a1x+2!a2x2...p!apxp(p=k1+k2..kn)
G(x)含义:ai 为选出 i 个物品的排列方法数
若题中有限定条件,只要把第i项出现的列在第i项的式中,未出现的不用列入式中
如: 物品 i 出现次数为非 0 偶数, 则原式改为 x^2 / 2! + x^4 / 4! + … + x^(ki / 2 * 2) / (ki / 2 * 2)!
例题:
有1,2,3,4四个数字组成的五位数中要求
数 1 出现次数不超过 2 次,但不能不出现;
2 出现次数不超过 1 次;
3 出现次数最多 3 次,可以不出现;
4 出现次数为偶数 (0, 2, 4)
求满足上述条件的数的个数:
根据可选数的范围求出每一个数字的指数型生成函数, 然后相乘, 化简
把最后得到的式子展开, 找到 x 5 x^5 x5 这一项, 转换为 x k / k ! x^k / k! xk/k! 的形式 (上下同时乘以5!..), 然后其系数为所求
模板题
有 n 种物品,每种物品有 ki 件,从中选出 m 件的排列数
public class Main {
public static final int N = 1000;
public static double[] sup = new double[N];
public static double[] tmp = new double[N];
public static int[] fact = new int[15];
public static int[] num = new int[N];
public static void main(String[] args) {
InputReader in = new InputReader();
fact[0] = 1;
for(int i = 1; i <= 10; i++) {
fact[i] = i * fact[i - 1];
}
int n, m;
n = Integer.parseInt(in.next());
m = Integer.parseInt(in.next());
for(int i = 1; i <= n; i++) {
num[i] = Integer.parseInt(in.next());
}
Arrays.fill(tmp, 0.0);
Arrays.fill(sup, 0.0);
for(int i = 1; i <= num[1]; i++) {
// 初始化系数
sup[i] = (1.0 / fact[i]);
}
for(int i = 2; i <= n; i++) {
for(int j = 0; j <= m; j++) {
for(int k = 0; j + k <= m && k <= num[i]; k++) {
// 和普通母函数不同, 这里的分母阶乘是系数
tmp[j + k] += (sup[j] / fact[k]);
}
}
for(int j = 0; j <= m; j++) {
sup[j] = tmp[j];
tmp[j] = 0.0;
}
}
int ans = (int)(sup[m] * fact[m] + 0.5);
System.out.println(ans);
}
static class InputReader {
StringTokenizer tok = null;
BufferedReader br = null;
InputReader() {
br = new BufferedReader(new InputStreamReader(System.in));
}
public boolean hasNext() {
while(tok == null || !tok.hasMoreElements()) {
try {
tok = new StringTokenizer(br.readLine());
} catch (Exception e) {
return false;
}
}
return true;
}
public String next() {
return hasNext() ? tok.nextToken() : null;
}
}
}
[;