2.5.3 例子:符号化的代数

2.5.3 例子:符号化的代数
符号代数表达式的操作是一个复杂的执行过程,它演示出诸多发生在大型系统的设计中的最难的问题。
一个代数的表达式,总之,能被表示为一个层次化的结构,是把操作符应用到操作数上的树。
我们能组装代数表达式以原生的对象的集合开始,例如常数和变量,组合它们,使用代数符号,
例如加法,乘法。像其它语言一样,我们能形成抽象,让我们有能力,以简单的术语引用复杂的对象。
在符号代数中的类型抽象是思想,例如线性组合,多项式,有理化的函数,三角函数。我们视这些
为复合对象,这常常是对表达式的处理的方向有用的。例如,我们能描述如下的表达式。

x^2*sin(y^2+1)+x*cos(2*y)+cos(y^3-2*y^2)

x的多项式,它的系数是y的多项式的三角函数,它的系数是整数.

这里我们不打算一个完整的代数操纵的系统.这样的系统是
极复杂的程序,内嵌了深度代数的知识和优雅的算法.
我们要做的是简单的看一下代数的操纵的重要部分:多项式算术.
我们将演示这样的系统的设计者面临的各种决策,为了有利于
组织这种努力,如何应用抽象数据和通用化的操作的思想.

* 多项式的算术
在设计一个系统时,为了执行多项式的算术,我们的首要任务是决定
一个多项式是什么.多项式一般是相对于特定的变量而定义的.
(它是多项式的自变量).简单来说,我们限定我们的多项式仅有一个自变量.
(单量的多项式).我们将定义一个多项式为各个项的和.任何一项都有一个
系数,自变量的幂,或者是系数与自变量的幂的乘积.一个系数被定义为一个
代数表达式,这个表达式不依赖多项式的自变量.例如:

5*x^2+3*x+7

以x为自变量的简单的多项式.

(y^2+1)*x^3+(2*y)*x+1
是一个以x为自变量的多项式.这个多项式它的系数是一个以Y为自变量的多项式。

我们已经正在接触一些棘手的问题。这些多项式中的第一个与多项式5*y^2+3*y+7
相同吗,还是不同?一个合理的答案是可能是的。如果我们认为一个多项式纯粹是
一个数学函数。但是如果我们认为一个多项式成为一个语法的形式,就不同了。
第二个多项式代数等价于以Y为自变量,以X的多项式为系数的多项式。
我们应该承认这一点吗,或者是不承认?进一步说,有其它的方式表示多项式,
例如一个多项式的因子们的积,或者是(对于一个自变量的多项式)根的集合,
或者是在一个特定的点的集合时,多项式的值的列表。通过解决在我们的代数的操纵的
系统中一个多项式是一个特定的语法形式,而不是它的潜在的数学的含义,
我们能处理上述的问题。

现在我们必须考虑如何在多项式上做算术。在这个简单的系统中,我们将
认为仅有加法和乘法。进而我们将坚持两个多项式都必须由相同的自变量
组合而成。

我们将使用数据抽象的相似的规则来进行我们的系统的设计。我们将表示多项式
使用一个叫保利(poly)的数据结构,它由一个变量和多项式的项的集合组成。
我们假定我们有选择子variable,term-list从一个保利中抽取出这些部分
和组装子make-poly, 它使用一个给定的变量和一个多项式的项的列表来组装保利。
一个变量是一个符号,所以我们能使用2.3.2部分中的程序same-variable?来比较变量。
如下的程序定义了保利的加法和乘法:

(define (add-poly p1 p2)
    (if (same-variable? (variable p1) (variable p2))
        (make-poly (variable p1)
            (add-terms (term-list p1) (term-list p2))
        )
 (error "Polys not in same var --ADD POLY" (list p1 p2))
    )
)

(define (mul-poly p1 p2)
   (if (same-variable? (variable p1) (variable p2))
       (make-poly (variable p1)
                  (mul-terms (term-list p1)
               (term-list p2)))
       (error "Polys not in same var --MUL POLY" (list p1 p2)))
)
 
为了把多项式集成到我们的通用化的算术系统中,我们需要提供它们结合类型的标签。
我们将使用标签polynomial,并且在操作的表格中以标签polynomial安装合适的操作。
我们在多项式的软件包中的一个安装程序中加入如下的代码,
与2.5.1部分的内容是相似的。

(define (install-polynomial-package)
   ;; internal procedures
   ;; representation of poly
   (define (make-poly variable term-list)  (cons variable term-list))
   (define (variable p) (car p))
   (define (term-list p) (cdr p))
   <same-variable? variable? >
   ;;
   <>
   ;;
   (define (add-poly p1 p2) ...)
   (define (mul-poly p1 p2) ...)
   ;;
   (define (tag p) (attach-tag 'polynomial p))
   (put 'add '(polynomial polynomial) (lambda (p1 p2) (tag (add-poly p1 p2))))
   (put 'mul '(polynomial polynomial) (lambda (p1 p2) (tag (mul-poly p1 p2))))
   (put 'make 'polynomial  (lambda (var terms) (tag (make-poly var terms))))
   'done
)

多项式的加法是执行项的加法。相同秩序(自变量的相同指数的幂)的项必须被组合。
这么做是通过形成相同幂级的新的项,它的系数是相加数的系数的和。一个加数的项
与其它的加数的项没有相同的幂级,就简单地累加到被组装的多项式的和中。

为了操纵多项式的项的列表,我们假定我们已经有了
一个组装子the-empty-termlist,它返回一个空的多项式的项的列表
,和一个组装子adjoin-term,它加一个项到项的列表中
我们还假定我们有
一个判断式 empty-termlist? 检查一个给定的多项式的项的列表是否是空的
一个选择子first-term,从一个项的列表中,抽取最高次的项。
一个选择子rest-terms,返回除了最高次的项以外的列表
为了操纵多项式的项,我们假定有
一个组装子make-term,给定项的次数和系数,组装一个多项式的项。
选择子order和 coeff ,相应地返回多项式的项的次数或者是系数。
这些操作允许我们把项和项的列表都视为数据抽象,我们能单独地考虑它们的表示法。

为了两个多项式的和,这是组装项的列表的程序:

(define (add-terms L1 L2)
  (cond ((empty-termlist? L1)  L2)
        ((empty-termlist? L2)  L1)
 (else (let ((t1 (first-term L1)) (t2 (first-term L2)))
            (cond ((> (order t1) (order t2))
            (adjoin-term t1
                  (add-terms (rest-terms L1) L2)))
           ((< (order t1) (order t2))
     (adjoin-term t2
                  (add-terms L1 (rest-terms L2))))
    (else (adjoin-term (make-term (order t1) (add (coeff t1)
                                                  (coeff t2)))
          (add-terms (rest-terms L1)
                     (rest-terms L2))))))))
)

这里最重要的一点是我们使用了通用的加法程序来加多项式的项的系数。
正如如下的我们看到的,这是很有力的结果。

为了两个多项式的项的列表的相乘,我们乘第一个列表中的任何一项
与其它列表的所有的项。重复使用mul-termby-all-terms,它乘一个给定的项与一个
给定的项的列表的所有的项。结果的项的列表被累加到一个和。
两个项相乘形成一个项,次数是两个因子的次数的和,它的系数是因子的系数的积。

(define (mul-terms L1 L2)
   (if (empty-termlist? L1)
       (the-empty-termlist)
       (add-terms (mul-term-by-all-terms (first-term L1) L2)
                  (mul-terms (rest-terms L1) L2)))
)

(define (mul-term-by-all-terms t1 L)
   (if (empty-termlist? L)
       (the-empty-termlist)
       (let ((t2 (first-term L)))
            (adjoin-term (make-term (+ (order t1) (order t2))
                             (mul (coeff t1) (coeff t2)))
                  (mul-term-by-all-terms t1 (rest-terms L))
     )
       )
   )
)

对于多项式的加法和乘法,这真的是所有的内容了。
注意的是,因为我们的操作是针对多项式的项,使用了通用化的操作
如加法和乘法,我们的多项式软件包能自动地处理系数的任何被
通用化算术软件包已知的类型。 如果我们包括了一个系数的机制,例如
在2.5.2部分中讨论的之一,那么我们能自动处理在不同的系数类型的
多项式的操作,例如:

[3*x^2+(2+3i)*x+7]*[x^4+(2/3)*x^2+(5+3i)]

因为我们安装了多项式的加法和乘法程序add-poly 和 mul-poly,在通用化的算术系统中,
为了类型的add加法和mul乘法操作,我们的系统也能自动处理如下的多项式。

[(y+1)*x^2+(y^2+1)*x+(y-1)]*[(y-2)*x+(y^3+7)]

原因是当系统在合并系数时,它将分发加法和乘法,因为系数本身是Y的多项式,
这将被合并使用add-poly 和 mul-poly程序。结果是面向数据的递归的一种方式。
例如一个对add-poly的调用,为了系数的相乘,结果导致对add-poly的递归调用。
如果系数的系数本身是多项式(正如可能被用来表示有三个变量的多项式),
数据导向确保了系统流向递归的另一个层级,并且能通过数据命令的结构的多个层级。

* 表示多项式的项的列表
最后,我们必须面向的工作是为了多项式的项的列表实现一个好的表示方法。
一个多项式的项的列表是,在效果上看,是被多项式的项的次数键值化的系数的集合。
因此,在2.3.3部分中讨论的任何表示集合的方法,都能应用于这个任务。另一个方面,
我们的程序add-terms和mul-terms,总是读取项列表顺序地从最高次到最低次。因此
我们将使用一些有序的列表表示法。

为了表示一个项的列表,我们应该怎么结构化这个列表呢?一个考虑是我们要操纵的多项式
的密度。如果在大多数次数的项都有非零的系数,我们就说这个多项式是密集的。如果有
太多的项的系数为零,我们说它是稀疏的。例如:

A: x^5+2*x^4+3*x^2-2*x-5
A是密集的,然而:
B:  x^100+2*x^2+1 是稀疏的。

密集的多项式的项的列表用系数的列表来表示是最有效率的。
例如,如上的A,能很好的表示为(1 2 0 3 -2 -5)。
在这种表示中,一个项的次数是开始于项的系数的子列表的长度,递减为1。
对于如B一样的稀疏的多项式,这是一个非常糟糕的表示法。这是一个只有很少的非零项,
由0填充的巨大的列表。一个稀疏的多项式的项列表的一个更合理的表示法是
作为一个非零项的列表,它的任何一项是一个列表,包括项的次数和那个次数的系数。
在这样的模式中,多项式B的有效的表示为((100 1) (2 2) (0 1))。
在大部分的多项式操作中,都是执行在稀疏多项式上。我们将使用这个方法。
我们假定项列表被表示为项的列表,以从最高次项到最低次项的顺序排列。
一旦我们已经做了这个决策,为了项和项的列表实现选择子和组装子就是很自然的事了:

(define (adjoin-term term term-list)
   (if (=zero? (coeff term))
       term-list
       (cons term term-list)
    )
)

(define (the-empty-termlist) '())
(define (first-term term-list) (car term-list))
(define (rest-terms term-lsit) (cdr term-list))
(define (empty-termlist? term-list) (null? term-list))
(define (make-term order coeff) (list order coeff))
(define (order term) (car term))
(define (coeff term) (cadr term))

=zero?被定义在练习2.80中的。
多项式的软件包的用户将创建有标签的多项式,用如下的程序:

(define (make-polynomial var terms)
((get 'make 'polynomial) var terms))

练习2.87
在通用化的算术软件包中为了多项式安装=zero?程序。这将允许
adjoin-term对系数本身是多项式的多项式也是有效的。

练习2.88
扩展多项式的系统来包括多项式的减法。(提示:为了定义一个通用化的减法操作,
你可能发现它是有帮助的)

练习2.89
定义一个程序,来实现如上描述的项列表的表示法,作为适合密集多项式的情况。

练习2.90
假定我们要有一个多项式系统对密集和稀疏的多项式都高效的。实现这个目标的一个
方式是在我们的系统中允许两种项列表的表示法。情况与2.4部分中的复数的例子是相似的。
为了实现这个目标,我们必须区别项列表的不同的类型,并且实现在项列表上的通用化的操作。
为了实现这个通用化,重设计多项式的系统。这是一个主体上的努力,不是一个局部的修改。

练习2.91
一个单变量的多项式能被另一个多项式进行除法操作,生成一个多项式商和一个多
项式余数。例如:

(x^5-1)/(x^2-1)=x^3+x, 余数是x-1

除法能被执行通过长除法。也就是,被除数的最高次项除以除数的最高次项。
结果是商的第一项。接下来,结果乘以除数,再从被除数减去这个值,通过递归
地用差值除以除数,来生成答案的其它部分。当除数的次数超过了被除数的次数时,
就递归停止了,这时的被除数为答案中的余数。而且,如果被除数一旦变成0,
返回商和余数都是0。

我们能设计一个程序div-poly在 add-poly和 mul-poly 模型之上.
程序能检查两个多项式是否有相同的变量.如果有,div-poly程序脱掉变量,
把问题传递给div-terms,它在项的列表上执行除法操作.设计div-terms
来计算除法的商和余数是很方便的.div-terms以两个项的列表为参数,返回一个
商的项的列表和一个余数的项的列表.

通过填写缺失的表达式,来完成下面的div-terms程序的定义.使用它来实现
div-poly程序,这个程序以两个多项式为参数,返回一个商和余数的多项式的列表.

(define (div-terms L1 L2)
  (if (empty-termlist? L1)
      (list (the-empty-termlist) (the-empty-termlist))
      (let ((t1 (first-term L1)) (t2 (first-term L2)))
           (if (> (order t2) (order t1))
        (list (the-empty-termlist) L1)
        (let ((new-c (div (coeff t1) (coeff t2)))
              (new-o (- (order t1) (order t2))))
             (let ((rest-of-result
           <递归地计算结果的剩余部分>))
           <形成完整的结果>)))))
)

* 符号化的代数中的类型的层次
我们的多项式的系统演示了在事实上一个类型(多项式)的对象怎么样成为
一个复杂的对象,也就是有多个不同的类型的对象作为它的一部分.在设计
通用化的操作中,这没有带来实际性的困难.我们仅需要安装合适的通用化操作
来为了复合类型的部分执行必要的操作.事实上,我们看到了多项式形成了一种递归的
数据抽象,即多项式的一部分可能本身也是一个多项式.我们的通用化操作和我们的面向
数据的编程风格能处理这种计算而没有什么困难之处.

另一个方面,多项式的代数,是一个系统,它的数据类型不能被自然地安排为一个塔.
例如,X的多项式的系数是Y的多项式是可能的.Y的多项式的系数是X的多项式也是可能的.
在任何自然的方式中,这些类型中的任何一个都不能是在其它类型的上面,把任何一个集合
中的元素加到一起常常是必要的.这么做有几种方式.一种可能是把一个多项式转换成其它的
类型,为了两个多项式都能有相同的自变量,通过扩展和重排项.
通过排序变量一个方式能强化成类塔式的结构,并且因为总是把任何多项式转换成一个
权威的形式,有最高优先权的变量为主,低优先权的变量放在系数中.这个策略工作
得很好,除了转换可能不必要的扩展一个多项式,让它更难读和可能工作的效率更低.
塔的策略对这种情况一定是不自然的,或者其它情况,就是用户能动态地发明出新的类型,
在各种组合形式中使用旧的类型,例如三角函数,幂,和整数.

在大规模的代数操纵的系统的设计中,控制强制类型转换是一个严重的问题,这并不让人吃惊.
这样的系统的复杂性的大部分是关于不同的类型之间的关系的.的确,说我们没有完全地理解
强制类型转换是对的.在事实上,我们还没有完全理解一个数据类型的概念.然而,
我们所知道的内容提供给我们强有力的结构和模块化原则来支持大型系统的设计.

练习2.92
通过在变量上强调排序,扩展多项式的软件包,让多项式的加法和乘法能工作在
多项式有不同的自变量的基础上(这并不容易!)

* 扩展的练习:有理化的函数
我们能扩展我们的通用化算术系统来包括有理化的函数.有一些小数它有
分子和分母,都是多项式.例如

(x+1)/(x^3-1)

这个系统应该能加法,减法,乘法,和除法有理化的函数,来执行如下的计算:

(x+1)/(x^3-1)+x/(x^2-1)=(x^3+2*x^2+3*x+1)/(x^4+x^3-x-1)

(通过消减公共的因子,这加法能被化简.普通的交叉的乘法能生成一个四度多项式
在五度多项式上)

如果我们修改我们的有理化算术的软件包,为了让它能使用通用化的操作,那么
它将能做我们所想要做的,除了化简小数为最简形式的问题.


练习2.93
修改有理数的算术软件包,来使用通用化的操作,却修改make-rat
为了不试着把小数化简为最简形式。通过调用make-rational以两个多项式为参数,
生成一个有理化的函数,来测试你的系统。

(define p1 (make-polynomial 'x '((2 1) (0 1))))
(define p2 (make-polynomial 'x '((3 1) (0 1))))
(define rf (make-rational p2 p1))

现在加rf到本身,使用add 。你能注意到这个加法程序没有把小数化简成最简形式。

我们能化简多项式的小数为最简形式,使用与我们用在整数中的相同的思想。
修改make-rat,用它们的最大公约因子除分子和分母。最大公约数的理念适用于多项式。
在事实上,我们能计算两个多项式的最大公约数,使用整数中用的欧拉算法。
整数版本如下:

(define (gcd a b)
  (if  (= b 0)
        a
 (gcd b (remainder a b))
  )
)

使用这个方法,我们能让定义GCD的明显修改来对项的列表有效:

(define (gcd-terms a b)
   (if (empty-termlist? b)
        a
 (gcd-terms b (remainder-terms a b))
   )
)

remainder-terms选择列表的余数部分,项的列表是除法操作
div-terms在练习2.91中实现的

练习2.94
使用div-terms,实现程序remainder-terms,并且使用这个程序
定义如上的gcd-terms.现在写一个程序gcd-poly来计算两个多项式的
GCD的多项式。(如果两个多项式没有共同的变量,程序应该能报错。)
在系统中安装一个通用化的操作greatest-common-divisor 来为多项式递归gcd-poly
并且为普通数的普通gcd.作为一个测试如下:

(define p1 (make-polynomial 'x '((4 1) (3 -1) (2 -2) (1 2))))
(define p2 (make-polynomial 'x '((3 1) (1 -1))))
(greatest-common-divisor p1 p2)

然后,手工地检查结果。

练习2.95
定义P1,P2,P3为多项式。

p1:  x^2-2*x+1
p2: 11*x^2+7
p3: 13*x+5

现在定义Q1是P1和P2的积,Q2是P1和P3的积,使用练习2.94中的
greatest-common-divisor来计算Q1和Q2的GCD。注意答案不同于P1.
这个例子把非整数操作引入计算,给GCD算法带来了困难。为了理解发生了什么,
当计算GCD时,试着跟踪gcd-terms,或者手工执行除法。

如果我们使用了GCD算法的如下的修改版本,我们能解决练习2.95中展示的问题。
(问题是它只对多项式的有整数系数的情况是有效的)在GCD的计算中,我们执行
任何多项式除法之前,我们以一个整数因子乘以被除数,在除法过程中,选择保证
没有小数部分。 我们的答案因此不同于实际的GCD.但是这无关于有理化函数的简化。
GCD能被用来除以分子和分母,所以整数常数的因子能被消除。

更精确的,如果P和Q是多项式,令O1是P的次数,O2是Q的次数,c是Q的主系数。
那么它能显示的是,如果我们把P乘以整数因子c^(1+O1-O2),结果多项式能被Q除,使用
div-terms算法,没有引入任何小数。对被除数乘以一个常数,然后再除,这叫做
伪除。除法的余数叫做伪余数。

练习2.96
a.实现程序pseudoremainder-terms, 它仅像remainder-terms除了调用div-terms之前
上述描述的乘因数。修改 gcd-terms来使用 pseudoremainder-terms,验证
greatest-common-divisor ,现在生成一个答案有整数的系数在练习2。95中的例子。

b.GCD现在有整数系数,但是它们比P1大。修改gcd-terms,
为了移除共同的因子,从答案的系数,通过对所有的系数除以最大的公约数。

因此,这里是如何把有理化的函数化简到最简形式:
   #计算分子和分母的GCD,使用练习2.96的gcd-terms的版本。
   #当你得到GCD,在除以GCD之前,使用相同的整数化因子乘以分子和分母,
为了在除GCD时不引入非整数的系数.正如你能使用的因子是GCD的主系数升到次数
1+O1-O2,O2是GCD的次数, O1是分子与分母的最大的次数。
   这保证在除分子和分母时没有引入任何小数。

   #这个操作的结果是分子和分母都有整数的系数。系数常常很大,
因为所有的整数化因子。所以最后的一步是移除冗余因子,通过计算
分子和分母的所有的系数的最大的公约数。再除这个因子。

练习2.97
a.实现这个算法,以程序reduce-terms 以n和d 这两个项的列表为参数,
返回列表nn dd。使用如上的算法n和d能化简到最简形式,也要写程序reduce-poly
与add-poly相似,检查两个多项式是否有相同的变量。如果有 ,reduce-poly脱掉变量,
把问题传递给reduce-terms,再把变量附加到reduce-terms返回的结果列表中。

b.定义一个程序与reduce-terms相似,做原生的make-rat对整数所做的事。

(define (reduce-integers n d)
  (let ((g (gcd n d)))
    (list (/ n g) (/ d g))
   )
)

并且定义reduce作为一个通用化的操作,调用 apply-generic来分发 reduce-poly(为多项式的参数)
和reduce-integers(为整数的参数)。在组合给定的分子和分母形成一个有理数之前,
通过使用make-rat来调用 reduce,你现在能很容易地使有理化的算术的软件包把小数化简成
最简的形式。系统现在能处理有理化的表达式,无论是整数还是多项式。为了测试你的系统,
在这个扩展练习开始时,试试这个例子:

(define p1 (make-polynomial 'x '((1 1) (0 1))))
(define p2 (make-polynomial 'x '((3 1) (0 -1))))
(define p3 (make-polynomial 'x '((1 1))))
(define p4 (make-polynomial 'x '((2 1) (0 -1))))

(define rf1 (make-rational p1 p2))
(define rf2 (make-rational p3 p4))
(add rf1 rf2)

看你是否得到了正确的答案,正确的化简到最简的形式。

GCD的计算是任何在有理化函数上有操作的系统的核心部分。使用如上的算法,
尽管数学上是自然的,但是是极其慢的。计算缓慢是部分归于有大量的除法操作,部分归于
大量的伪除的中间的系数的生成。代数的操纵的系统的开发的活跃的领域之一是
为了计算多项式的GCD的更好的算法的设计。

猜你喜欢

转载自blog.csdn.net/gggwfn1982/article/details/81529877