当前位置: 首页 > news >正文

SICP 习题 (1.14)解题总结

SICP 习题 1.14要求计算出过程count-change的增长阶。count-change是书中1.2.2节讲解的用于计算零钱找换方案的过程。


要解答习题1.14,首先你需要理解count-change的工作方式,要理解count-change的工作方式,最好是自己去实现一遍count-change。


为了避免自己直接抄书中的代码,我决定自己实现一遍用来找换人民币的的“count-change”。事实上,我在看完并理解count-change的代码后,当我去实现人民币版的“count-change”时,我就强制自己不再回去看“count-change”的代码,保证自己有更多的主动思考。

有意思的是,当我实现完了回去看书上的代码,发现两者还是有挺大的区别,虽然算法是一样的,结果也都正确的。


首先我们得有个过程遍历各种零钱,我为了简化程序,只做了“元”的找换,“分”和“角”就略过了。


遍历各种零钱的过程如下,就是遍历“1元”,“2元”,“5元”,“10元”,“20元”,“50元”,“100元”这几种零钱。


这里就和书上有点差别,书上是记录现在还可以使用几种零钱,根据可以使用的零钱种类数量返回其中最大面值的金额。


我是简单粗暴地从最小面值遍历到最大面值。


(define (RMB-Change-Next-Kind change-kind)
  (cond ((= change-kind 1) 2)
	((= change-kind 2) 5)
	((= change-kind 5) 10)
	((= change-kind 10) 20)
	((= change-kind 20) 50)
	((= change-kind 50) 100)
	(else 0)))


书中讲到的找换零钱的方法基于以下基本思路:


比如我们需要将100块的人民币找换成零钱,有几种找换方式?可以简单分为两种:有使用1块面额的,和没有使用1块面额的。


对于有使用1块面额的,把这一块钱去掉,剩99块,我们又可以继续看99块有几种找换方式。找到99块的所有找换方式,在加上现在这里去掉的1块钱,就是100块找换方式中有使用1块的所有找换方式。


对于没有使用1块面额的,我们就需要看看100块找成其它面额(不包含1块)的找换方式。


然后将上面两种类型的数量加起来就是100块的所有找换方式。


可以看到,以上的思路对应的是树形递归,每次递归调用分两路,一路调用的找换总额减少,一路调用的找换币种减少。


我的实现方法是下面这样的,和书上略有不同。


(define (RMB-Change-Recursive amount change-kind change-result)
  (if (= amount 0) (format #t "Got one: ~S~%" change-result))
  (cond ((= amount 0) 1)
	((< amount 0) 0)
	((= change-kind 0) 0)
	(else (+ (RMB-Change-Recursive amount (RMB-Change-Next-Kind change-kind) change-result)
		 (RMB-Change-Recursive (- amount change-kind) change-kind (cons change-kind change-result))))))


如果对上面的过程不理解,建议回去读书中的1.2.2节,读懂为止。


好的,现在才到我们真正的题目了,第一个是找出11块的找换方式,这种比较容易,可以按以上的方式手工列出,也可以直接执行代码看看结果。


问题的第二部分比较麻烦,就是问,对于现金量的增长,以上过程的空间增长阶和时间增长阶是什么?


如果对增长阶的概念没有理解透,这题很难解。


我们先开始分析一下,如果发现过程中有不理解的,需要回去书上看看增长阶的内容。


首先来看看空间增长阶,当我们需要计算12块的找换方式时,比计算11块的找换方式需要增加多少内存?


当然,空间增长阶不能简单理解为需要增加多少内存,可能会有其它的空间需要,或者人家用纸算,根本不用内存,我们在这里就粗暴地将空间理解为内存吧。


增加多少内存呢?也不需要计算准确数字,只要计算大概的敏感度就好了。比如增长是线性的,多计算1块钱就多需要100K左右的空间。又或者是二次方曲线的增长,计算11块时需要11的二次方的空间,二计算12块时需要12的二次方的空间。这些就是我们要求的增长阶。


对于书中的count-change过程,空间增长阶是多少呢?


对于递归过程的空间增长阶,一般是去计算递归计算过程的最深深度。因为过程调用完以后,其使用的空间是会被释放的,我们需要计算的是被那些一直递归调用自己,目前还没有返回的过程所占用的空间。


同时需要注意每次递归调用的参数有没有累加,累加的形式是什么。


以上过程的递归嵌套最深的就是全部用1块来找换,嵌套深度就是要找换的金钱量,11块就是嵌套了11层,100块就是嵌套100层。而调用过程中参数没有累加,只是不断替换而已。


所以,count-change过程的空间增长阶是Theta[n]。


不过,我这里设计的过程好像有点不一样,为了打印所有可能的找换方式,我定义了一个参数用于保存目前计算过的暂时可行的零钱组合,就是参数change-resule。这个参数占用的空间是如何变化的呢?


可以发现,这个参数中的元素最多的时候就是全部找成1元的时候,这时候change-result中的值是:(1 1 1 …. 1)共有n个。而每次递归调用都有一个列表需要暂时保留,所以,当递归调用到最深层的时候,其实有n个列表,分别是(1) (1 1) (1 1 1) ….. (1 1 1 1 …. 1 1),所以这里占用的空间是1到n的累加。


所以我的RMB-Change过程的空间增长阶是Theta[n的累加],因为在求增长阶的时候只取多项式中最高阶的那项,Theta[n]就被忽略了。


以上是空间增长阶,那么步数的增长阶呢?或者说是时间增长阶呢?


书中习题要求的是步数的增长阶,我们一般假设执行计算的每一步都消耗等同的时间,所以时间的增长阶和步数的增长阶应该是一致的。有关这个假设是否正确后面的习题还有详细的讨论,目前暂时认为时间增长阶和步数的增长阶相同。


这个增长阶的求解过程比较复杂,我也到网上参考了好多别人的解法,有许多种思路。我总结了一个我个人比较容易理解的解法,描述如下:


以我这里的例子,找换面额种类有7种,分别是“1元”,“2元”,“5元”,“10元”,“20元”,“50元”,“100元”。

为了方便表达,我们用函数(T n m)来表示以上过程的时间复杂度,其中n是需要找换金额总数量,m是用于找换的零钱的面额的种类数量。


如果把1000元钱进行找换,可以把1000元的找换方式分为 “使用1元的” 和 “不使用1元的” 两种方式,就是:

(T 1000 7) = (T 999 7) + (T 1000 6)


而999元的找换方式又可以分为 “使用1元的” 和 “不使用1元的” 两种方式,就是:

T 999 7) = (T 998 7) + (T 999 6)


如此不断分解,就可以得到1000个分支,每个分支带一个(T n 6)。

也就是所这里的(T n 7) = 1000 * (T n 6) = n * (T n 6)


再来计算(T n 6),在这里也就是( T 1000 6)

就是把1000元找换成“2元”,“5元”,“10元”,“20元”,“50元”,“100元”这6种零钱。


按上面相同的方法有:

(T 1000 6) = (T 998 6) + (T 1000 5)

(T 996 6) = (T 996 6) + (T 1000 5)

(T 994 6) = (T 994 6) + (T 1000 5)

....

按以上方法可以拆出500个分支(就是n/2个分支,因为这次是从2元开始找),每个分支带一个(T 1000 5)。


就有(T 1000 6) = n/2 * ( T 1000 5)


以此类推就有(T 1000 7) = n * ( n/2 * ( n/5 * ( n/10 * ( n/20 * (n/50 * (n/100)))))),其中的除数就是各个面额。


就有(T 1000 7 ) = (n 的 7次方)/10000000


因为我们在求增长阶,所以直接取增长阶为 (n 的 7次方)


虽然除数10000000也算是个很大的数,不过在n大到一定程度时,n 的 7次方)就变成一个超级大的数,这时10000000也就不算什么了。


这同时也符合我们观察到的现象,当n很小时,实际需要的步数远小于n 的 7次方)。



相关文章:

  • 一个爬电商数据并实现搜索的例子
  • Qt5学习之路(VS下Qt设计师文件的使用)2013-10-13
  • 使用shell关闭占用某一个端口的程序
  • CGlib的动态代理使用示例
  • (4)事件处理——(6)给.ready()回调函数传递一个参数(Passing an argument to the .ready() callback)...
  • (4)事件处理——(7)简单事件(Simple events)
  • 把编程语言比喻为人体
  • Qt5学习之路(vs2012下创建一个QT应用程序)2013-10-14
  • 说说我在家乡山东日照的面试经历以及对家乡互联网产业的一些认识吧
  • mysql实现随机查询
  • SICP 习题 (1.15) 解题总结
  • 编码规范之美.佛语释道
  • 小智慧63
  • 自己写Lucene分词器原理篇——CJKAnalyzer简单讲解
  • 如何建立基于CocoaPods的ReactiveCocoa工程
  • JS 中的深拷贝与浅拷贝
  • AngularJS指令开发(1)——参数详解
  • Centos6.8 使用rpm安装mysql5.7
  • Effective Java 笔记(一)
  • Netty 4.1 源代码学习:线程模型
  • TiDB 源码阅读系列文章(十)Chunk 和执行框架简介
  • Traffic-Sign Detection and Classification in the Wild 论文笔记
  • Vue UI框架库开发介绍
  • vue从创建到完整的饿了么(11)组件的使用(svg图标及watch的简单使用)
  • 闭包--闭包之tab栏切换(四)
  • 程序员该如何有效的找工作?
  • 从setTimeout-setInterval看JS线程
  • 对超线程几个不同角度的解释
  • 基于HAProxy的高性能缓存服务器nuster
  • 警报:线上事故之CountDownLatch的威力
  • 聊聊directory traversal attack
  • 猫头鹰的深夜翻译:Java 2D Graphics, 简单的仿射变换
  • 前端攻城师
  • 我的业余项目总结
  • 用mpvue开发微信小程序
  • ​【原创】基于SSM的酒店预约管理系统(酒店管理系统毕业设计)
  • ​Base64转换成图片,android studio build乱码,找不到okio.ByteString接腾讯人脸识别
  • ​业务双活的数据切换思路设计(下)
  • (31)对象的克隆
  • (aiohttp-asyncio-FFmpeg-Docker-SRS)实现异步摄像头转码服务器
  • (C语言)深入理解指针2之野指针与传值与传址与assert断言
  • (env: Windows,mp,1.06.2308310; lib: 3.2.4) uniapp微信小程序
  • (Matalb回归预测)PSO-BP粒子群算法优化BP神经网络的多维回归预测
  • (ZT) 理解系统底层的概念是多么重要(by趋势科技邹飞)
  • (保姆级教程)Mysql中索引、触发器、存储过程、存储函数的概念、作用,以及如何使用索引、存储过程,代码操作演示
  • (笔试题)合法字符串
  • (附源码)springboot 智能停车场系统 毕业设计065415
  • (力扣题库)跳跃游戏II(c++)
  • (七)Java对象在Hibernate持久化层的状态
  • (三维重建学习)已有位姿放入colmap和3D Gaussian Splatting训练
  • (原創) 系統分析和系統設計有什麼差別? (OO)
  • (正则)提取页面里的img标签
  • (转)GCC在C语言中内嵌汇编 asm __volatile__
  • (转)Java socket中关闭IO流后,发生什么事?(以关闭输出流为例) .
  • (转载)Linux网络编程入门