算法笔记四 排队买票

题干

描述
有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的排列。
我们假设:

  1. 前 2P 个小孩组成一个合法的排队,且持有 1 元的小孩和持有 2 元的小孩数量相等,皆为 P。(P = 0, 1, 2…)
  2. 第 2P + 1 个小孩持有 2 元。

于是我们可以把非法排队分为 3 部分:

  1. 前 2P 个小孩
  2. 第 2P + 1 个小孩
  3. 剩下的小孩,假设共 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=0​K​​K(P)A(​N​P​​)A(​K​P​​)(K−P)R! P=0KK(P)A(NP)A(KP)(KP)R!P=0KK(P)A(NP)A(KP)(KP)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=0​K​​K(P)A(​N​P​​)A(​K​P​​)(K−P)R! M!P=0KK(P)A(NP)A(KP)(KP)R!M!P=0KK(P)A(NP)A(KP)(KP)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)=nbuy(n1,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)={nbuy(n1,k,1)nbuy(n1,k,y+1)+kbuy(n,k1,y1)y=0y=0

有了递归公式, 接下来需要解决递归的出口.
显然buy(n, k, y)中的n, k, y都不可能是负数, 也就是说n, k只要有一个递归到0, 就必须结束.

那么我们先考虑 n ≠ 0 , k = 0 n \not= 0, k=0 n=0k=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=0k=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<kyk

综上,我们总结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)=nbuy(n1,k,1)nbuy(n1,k,y+1)+kbuy(n,k1,y1)A(nn)A(kk)0y=0,n=0,k=0y=0,n=0,k=0n=0,k=0n=0,k=0,ykn=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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

对象被抛出

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值