题干
描述
有M个小孩到公园玩,门票是1元。其中N个小孩带的钱为1元硬币,K个小孩带的钱为2元纸币,而售票员没有零钱。问这些小孩共有多少种排队方法,使得售票员总能找得开零钱。注意:两个小孩,他们的位置互换,也算是一种新的排法。
输入
输入一行,M,N,K(其中M=N+K,M<=20).
输出
输出一行,总的排队方案。
思路
首先应该注意到, 由于售票员没有零钱, 故收的2元纸币只能通过收的1元硬币来找零, 也即是说, 如果1元硬币的数量N, 小于2元纸币的数量K, 肯定不存在合法的排列.
解法一
其次, 最先能想到的解法应该是暴力破解, 即将M!种排列组合一一枚举.毫无技术含量的算法, 这里不再详细说明实现.
解法二(转载自https://blog.dotcpp.com/a/64305)
再深入想一想, 当N==K时, 此题变为将数量相等的1,2两个数进行排列组合, 完全可以用卡特兰数求解,. 当N<K时无解. 当N>K时, 可以用总的排列数量M!, 减去非法的排列数量, 问题即转变为求非法的排列数量.
现在的问题即什么样的排列是非法的. 可以肯定的是, 非法排列中2的个数一定至少比1的个数多1. 那么我们就取2的个数比1多1的排列。
我们假设:
- 前 2P 个小孩组成一个合法的排队,且持有 1 元的小孩和持有 2 元的小孩数量相等,皆为 P。(P = 0, 1, 2…)
- 第 2P + 1 个小孩持有 2 元。
于是我们可以把非法排队分为 3 部分:
- 前 2P 个小孩
- 第 2P + 1 个小孩
- 剩下的小孩,假设共 R 个(R = M - 2P - 1)
前 2P 个小孩组成一个合法排队,且满足:M’ = 2P,N’ = K’ = P。
于是排队数可以用卡特兰数计算。
第 2P + 1 个小孩要持有 2 元,由于前 2P 个小孩中已经用掉 P 个持有 2 元的小孩,此处还有 K - P 种选择。
最后 R 个小孩的排队方式不影响整体性质,所以全排列。公式为:
∑
P
=
0
K
K
(
P
)
A
(
N
P
)
A
(
K
P
)
(
K
−
P
)
R
!
∑
P
=
0
K
K
(
P
)
A
(
N
P
)
A
(
K
P
)
(
K
−
P
)
R
!
\sum_{P=0}^KK(P)A{(_N^P)}A{(_K^P)}(K-P)R!∑P=0KK(P)A(NP)A(KP)(K−P)R!
P=0∑KK(P)A(NP)A(KP)(K−P)R!∑P=0KK(P)A(NP)A(KP)(K−P)R!
合法的排队方法数就等于总的方法数减去非法的方法数:
M
!
−
∑
P
=
0
K
K
(
P
)
A
(
N
P
)
A
(
K
P
)
(
K
−
P
)
R
!
M
!
−
∑
P
=
0
K
K
(
P
)
A
(
N
P
)
A
(
K
P
)
(
K
−
P
)
R
!
M!-\sum_{P=0}^KK(P)A{(_N^P)}A{(_K^P)}(K-P)R!M!−∑P=0KK(P)A(NP)A(KP)(K−P)R!
M!−P=0∑KK(P)A(NP)A(KP)(K−P)R!M!−∑P=0KK(P)A(NP)A(KP)(K−P)R!
解法三 递归
我们设函数buy(n, k, y), 其中n为1元硬币的数量, k为2元纸币的数量, y为售票员的起始找零, 该函数计算n个1元, k个2元, 起始找零为y元的条件下, 合法的排列数量.
对于buy(n, k, y), y==0时, 不难发现排在第一个的只能是1(n个1, 任取一个, 共n种情况), 如果第一个是2那将无法找零. 此时我们将这个排在第一个的1, 视为下一步递归的初始找零, 因为这是售票员可以直接收走的.那么此时的情况为, n-1个1元硬币, k个2元纸币, 起始找零为1元. 即buy(n-1, k, 1). 也就是说,
b
u
y
(
n
,
k
,
0
)
=
n
∗
b
u
y
(
n
−
1
,
k
,
1
)
buy(n, k, 0) = n*buy(n-1, k, 1)
buy(n,k,0)=n∗buy(n−1,k,1).
那么当
y
≠
0
y\not=0
y=0时, 显然1和2都可以排在第一个. 不同之处在于, 1排第一个, 下一步递归的y+1; 2排在第一个, 那么y-1;
总结, 我们得到递归公式如下:
b u y ( n , k , y ) = { n ∗ b u y ( n − 1 , k , 1 ) y = 0 n ∗ b u y ( n − 1 , k , y + 1 ) + k ∗ b u y ( n , k − 1 , y − 1 ) y ≠ 0 buy(n, k, y) = \begin{cases} n*buy(n-1, k, 1) & y = 0 \\ n*buy(n-1, k, y+1)+k*buy(n, k-1, y-1) & y \not= 0 \end{cases} buy(n,k,y)={n∗buy(n−1,k,1)n∗buy(n−1,k,y+1)+k∗buy(n,k−1,y−1)y=0y=0
有了递归公式, 接下来需要解决递归的出口.
显然buy(n, k, y)中的n, k, y都不可能是负数, 也就是说n, k只要有一个递归到0, 就必须结束.
那么我们先考虑
n
≠
0
,
k
=
0
n \not= 0, k=0
n=0,k=0. 此时意味着, 只有n个带1元硬币的小孩, 无论怎么排队, 都是合法的, 故
b
u
y
(
n
,
0
,
y
)
=
A
(
n
n
)
buy(n, 0, y)=A{(_n^n)}
buy(n,0,y)=A(nn)
其次我们考虑
n
=
0
,
k
≠
0
n = 0, k \not= 0
n=0,k=0. 此时意味着, 只有k个带2元纸币的小孩, 那么售票员至少需要k元的起始找零, 否则不存在合法排列.故
b
u
y
(
0
,
k
,
y
)
=
{
0
y
<
k
A
(
k
k
)
y
≥
k
buy(0, k, y) = \begin{cases} 0 & y < k \\ A{(_k^k)} & y \geq k \end{cases}
buy(0,k,y)={0A(kk)y<ky≥k
综上,我们总结buy函数的递归公式如下
b
u
y
(
n
,
k
,
y
)
=
{
n
∗
b
u
y
(
n
−
1
,
k
,
1
)
y
=
0
,
n
≠
0
,
k
≠
0
n
∗
b
u
y
(
n
−
1
,
k
,
y
+
1
)
+
k
∗
b
u
y
(
n
,
k
−
1
,
y
−
1
)
y
≠
0
,
n
≠
0
,
k
≠
0
A
(
n
n
)
n
≠
0
,
k
=
0
A
(
k
k
)
n
=
0
,
k
≠
0
,
y
≥
k
0
n
=
0
,
k
≠
0
,
y
<
k
buy(n, k, y) = \begin{cases} n*buy(n-1, k, 1) & y = 0, n \not= 0, k \not= 0 \\ n*buy(n-1, k, y+1)+k*buy(n, k-1, y-1) & y \not= 0, n \not= 0, k \not= 0 \\ A{(_n^n)} & n \not= 0, k=0 \\ A{(_k^k)} & n=0, k \not= 0, y \geq k \\ 0 & n=0, k \not= 0, y<k \end{cases}
buy(n,k,y)=⎩⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎧n∗buy(n−1,k,1)n∗buy(n−1,k,y+1)+k∗buy(n,k−1,y−1)A(nn)A(kk)0y=0,n=0,k=0y=0,n=0,k=0n=0,k=0n=0,k=0,y≥kn=0,k=0,y<k
代码实现
解法二 卡特兰数
#include<iostream>
using namespace std;
// 计算排列数
int a(int a1, int a2){
if (a2 == 0) return 1;
a2--;
int pro = a1;
for (int i = 0; i < a2; i++){
a1--;
pro *= a1;
}
return pro;
}
// 计算组合数
int c(int c1, int c2){
return a(c1, c2) / a(c2, c2);
}
// 计算卡特兰数
int catalan(int n){
return c(2 * n, n) / (n + 1);
}
int main(){
int m, n, k;
cin >> m >> n >> k;
if (n < k) cout << 0;
else{
int sum = 0;
for (int p = 0; p <= k; p++){
int r = m - 2 * p - 1;
int nogood = catalan(p) * a(k, p) * a(n, p) * (k - p) * a(r, r);
sum += nogood;
}
cout << a(m, m) - sum << endl;
}
return 0;
}
解法三 递归
#include <iostream>
using namespace std;
//n的阶乘
int factor(int n){
int fac = 1;
for (int i = 2; i <= n; i++) fac *= i;
return fac;
}
int buy(int n, int k, int y) {
if (0 != n && 0 != k) {
if (0 == y) return n * buy(n - 1, k, 1); // 第一个排队的只能是1元, n种可能, 初始零钱为1
else return n*buy(n-1, k, y+1) + k*buy(n, k-1, y-1); // 两种可能, 第一个排队的可以是1元也可以是2元
}else{ // n 和 k 至少一个为0
if (0==n && 0!=k){ // k!=0
if (y < k) return 0; // k个2元, 至少需要k元的初始找零, 不然无法排队
else return factor(k);
}else if(0==k && 0!=n){
return factor(n);
}else return 0;
}
}
int main(){
int m, n, k;
cin >> m >> n >> k; // 至少需要k个1元, 所以如果n<k, 不存在合法的排列
if (0 == m) cout << 0 << endl;
else if (n < k) cout << 0 << endl;
else cout << buy(n, k, 0) << endl; // 初始找零为0元
return 0;
}