从《编程之美》买票找零问题说起,娓娓道来卡特兰数——兼爬坑指南

本文详细介绍了《编程之美》中关于卡特兰数的问题,包括买票找零、入栈出栈、矩阵连乘、街区对角线和圆上点对互连等问题的解决方法。作者指出书中公式对于某些问题的局限性,并通过母函数法推导了一般性的卡特兰数公式,讨论了多边形划分和二叉树构造问题。此外,还探讨了程序实现的两种方法:公式法和递推法,并比较了它们的优缺点。文章旨在帮助读者理解和解决更广泛的问题,避免掉入《编程之美》中可能存在的陷阱。
摘要由CSDN通过智能技术生成

引子:

  大约两个月前,我在练习一些招聘的笔试题中,有一道和卡特兰数相关。那时还没来得及开始仔细看《编程之美》,就先翻到那一章节,草草地看了下买票找零的例子和证明并把书上的背下来了事。当然,只靠这个式子是可以解决一些问题的,但不知是《编程之美》的作者有意挖的陷阱来甄别所谓的“Poser”,还是疏忽了没有进一步讨论,又或者是因限于篇幅而将更本质的东西留给感兴趣的读者来挖掘,对于能用一般卡特兰数解决的问题,这个特殊的式子是解决不了的。当然,做为一本上市了很多年的书,《编程之美》上的各个问题在网络上都能找到相关的讨论和文章,更不用说卡特兰数——自发现至今已有200年左右——的相关资料更是比比皆是。因此,本文的主要目的不是向读者再一次地介绍卡特兰数的性质和应用,而是帮助读者跨越《编程之美》留下的陷阱,找寻更一般化的卡特兰数的公式,从而解决更一般的问题。

 

内容提要(点击跳转;博文右下角的回到顶部的链接,可回到本页):

 

 阅读建议:

  • 读过《编程之美》?正如引子提到的,本文希望能使掉到《编程之美》的陷阱里的读者能够顺利爬坑。如果你没掉坑里,恭喜你,我们可以一起交流下这个坑是什么样的。不过对于街区不跨越对角线问题,不知道你是否与我一样多想了一点东西?
  • 没读过《编程之美》?那么你只需提高警惕,文中会告诉你坑在哪里,而不是让你先跳下去再爬出来。
  • 没有学习过母函数是什么和怎么使用?那么用母函数证明的过程就不必太在意了,其实用数学归纳法也可以证明。我把证明写在上面的主要原因是增强读者的信心:这些东西并不是凭空而来。
  • 之前读过卡特兰数,想直接做练习?没关系,如果做题的过程中发现自己理解有问题可以再去文中相应的解释去看嘛。
  • 想直接了解如何编程解决?没问题,最后有两段很好理解的代码,捎带进行了分析。

  [回到索引 或继续阅读]

 

《编程之美》的卡特兰数

  先简单概括一下《编程之美》是如何引入和介绍卡特兰数的。

问题(《编程之美》4.3买票找零):2n个人排队买票,其中n个人持50元,n个人持100元。每张票50元,且一人只买一张票。初始时售票处没有零钱找零。请问这2n个人一共有多少种排队顺序,不至于使售票处找不开钱?

分析1:队伍的序号标为0,1,...,2n-1,并把50元看作左括号,100元看作右括号,合法序列即括号能完成配对的序列。对于一个合法的序列,第0个一定是左括号,它必然与某个右括号配对,记其位置为k。那么从1到k-1、k+1到2n-1也分别是两个合法序列。那么,k必然是奇数(1到k-1一共有偶数个),设k=2i+1。那么剩余括号的合法序列数为f(2i)*f(2n-2i-2)个。取i=0到n-1累加,

并且令f(0)=1,再由组合数C(0,0)=0,可得

至于怎么推导,《编程之美》就没有详细地说明,而是用另一个角度来解释,这个解释类似于《计算机程序设计艺术(卷一)》2.2.1节习题4的解答提到的精彩解法“反射原理”,下面是对其的概括(我所知的最早的出处是:http://bbs.csdn.net/topics/320099239

 

问题大意是用S表示入栈,X表示出栈,那么合法的序列有多少个(S的个数为n)
显然有c(2n, n)个含S,X各n个的序列,剩下的是计算不允许的序列数(它包含正确个数的S和X,但是违背其它条件).
在任何不允许的序列中,定出使得X的个数超过S的个数的第一个X的位置。然后在导致并包括这个X的部分序列中,以S代替所有的X并以X代表所有的S。结果是一个有(n+1)个S和(n-1)个X的序列。反过来,对一垢一种类型的每个序列,我们都能逆转这个过程,而且找出导致它的前一种类型的不允许序列。例如XXSXSSSXXSSS必然来自SSXSXXXXXSSS。这个对应说明,不允许的序列的个数是c(2n, n-1),因此an = c(2n, n) - c(2n, n-1)。

 

这个解法正好能适用于一种特殊情况,以下是对其的叙述:

n+m个人排队买票,并且满足n \ge m,票价为50元,其中n个人各手持一张50元钞票,m个人各手持一张100元钞票,除此之外大家身上没有任何其他的钱币,并且初始时候售票窗口没有钱,问有多少种排队的情况数能够让大家都买到票。

 

这个题目是Catalan数的变形,不考虑人与人的差异,如果m=n的话那么就是我们初始的Catalan数问题,也就是将手持50元的人看成是+1,手持100元的人看成是-1,任前k个数值的和都非负的序列数。

 

这个题目区别就在于n>m的情况,此时我们仍然可以用原先的证明方法考虑,假设我们要的情况数是D_{n+m},无法让每个人都买到的情况数是U_{n + m},那么就有

可以使用中缀表达式转后缀表达式的方法,然后使用栈计算后缀表达式的值。 具体步骤如下: 1. 定义一个栈用于存储运算符和数字,一个列表用于存储后缀表达式。 2. 遍历中缀表达式,如果是数字,则直接加入后缀表达式列表中;如果是左括号,则加入栈中;如果是右括号,则将栈中的运算符弹出,加入后缀表达式列表中,直到遇到左括号;如果是运算符,则将栈中优先级大于等于该运算符的运算符弹出,加入后缀表达式列表中,最后将该运算符入栈。 3. 遍历后缀表达式列表,如果是数字,则入栈;如果是运算符,则将栈顶的两个数字弹出,进行计算,并将结果入栈。 4. 栈中最后剩余的数字即为表达式的值。 代码实现如下: ```python def infix_to_postfix(expr): stack = [] # 运算符栈 postfix = [] # 后缀表达式 priorities = {'(': 0, '+': 1, '-': 1, '*': 2, '/': 2} # 运算符优先级 for token in expr: if token.isdigit(): # 如果是数字,直接加入后缀表达式 postfix.append(token) elif token == '(': stack.append(token) # 如果是左括号,入栈 elif token == ')': while stack and stack[-1] != '(': postfix.append(stack.pop()) # 如果是右括号,弹出栈中的运算符,并加入后缀表达式,直到遇到左括号 if stack and stack[-1] == '(': stack.pop() # 弹出左括号 else: while stack and priorities.get(stack[-1], -1) >= priorities.get(token, -1): postfix.append(stack.pop()) # 如果是运算符,弹出栈中优先级大于等于该运算符的运算符,并加入后缀表达式 stack.append(token) # 将该运算符入栈 while stack: postfix.append(stack.pop()) # 将栈中剩余的运算符加入后缀表达式 return postfix def eval_postfix(postfix): stack = [] # 数字栈 for token in postfix: if token.isdigit(): stack.append(int(token)) # 如果是数字,入栈 else: b = stack.pop() # 弹出栈顶的两个数字 a = stack.pop() if token == '+': stack.append(a + b) # 进行计算,并将结果入栈 elif token == '-': stack.append(a - b) elif token == '*': stack.append(a * b) elif token == '/': stack.append(a / b) return stack[-1] # 栈中剩余的数字即为表达式的值 expr = input('请输入含有四则运算带小括号的表达式:') postfix = infix_to_postfix(expr) value = eval_postfix(postfix) print('表达式的值为:', value) ``` 示例输入:`(1+2)*3-4/2` 示例输出:`表达式的值为:8.0`
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值