在函数式语言中理解多项式除法算法

3

这是一个采用相当古老的语言标准ML实现的算法。我难以理解这个算法:

fun polyquotremd ts ((n,b) :: us) = 
    let fun quo [] qs = (rev qs, [])
        | quo ((m,a) :: ts) qs = 
            if m < n then (rev qs, (m,a) :: ts)
            else
                quo (polysum ts
                    (map (termproduct(m-n, ~a/b)) us))
                    ((m-n, a/b) :: qs)
    in
        quo ts []
    end;

这是目标。我们通过使用元组列表来密集表示多项式,每个元组包含指数在索引0处和系数在索引1处。例如,$x^2 + 1$ 表示为 [(2, 1.0), (0, 1.0)]。这由下面的辅助函数中的类型(int*real)list给出。我们想要获得一个输出(quo, rem),每个都是指示商和余数的元组列表。 ts 是要被除的多项式,而us 是除数。
这是我想象中算法的工作方式:
  1. 首先,如果ts = [],那么这意味着我们没有多项式可以除以,因此我们返回qs,我们的累积商结果作为答案。由于我们不断将((m-n, a/b) :: qs)添加到qs列表的头部,因此需要反转列表以使其以最大指数开头。

  2. 第一个if条件处理除数的指数n大于我们正在处理的当前幂m的情况。在这种情况下,我们只需不添加该元素并返回(rev qs, (m,a) :: ts),即(quo, rem)

  3. else条件是我感到困惑的部分。我知道第二个括号((m-n, a/b) :: qs)是我们要添加的累积结果,但polysum ts (map (termproduct(m-n, ~a/b)) us))实际上在做什么?背后的数学是什么?

通常,我们会一次性将多项式除数相除,即一起除以$x+1$。但是这个算法首先使用$x$进行除法,然后再使用$1$。这怎么可能行得通?为什么有~a/b(对您的参考,意味着-a/b)。
辅助函数:
fun take ([], _) = []
| take (x::xs, i) = if i>0
then x :: take(xs, i-1)
else [];

fun drop ([], _) = []
| drop (x::xs, i) = if i>0 then drop(xs,i-1)
else x::xs;

fun termproduct (m,a) (n,b) = (m+n, a*b) : (int*real);

fun polyproduct [] us = []
    | polyproduct [(m,a)] us = map (termproduct(m,a)) us
    | polyproduct ts us =
    let 
        val k = length ts div 2
    in 
        polysum (polyproduct (take(ts,k)) us)
                (polyproduct (drop(ts,k)) us)
    end;

有人能给我解释一下第三点吗?我对这个算法的数学部分非常困惑。但它确实起作用..顺便说一句,这段代码可以编译,并且您可以在标准ML中测试它。


1
(添加了Haskell标签,因为许多使用非常类似的语言的聪明人都会查看它 :)) - גלעד ברקן
2
标签不是用来“让更多人查看问题”的。它们是用来表示“这个标签所代表的实体与此问题有关”。 - jscs
1
@JoshCaswell 如果我想让更多的人看到这个问题,我会添加“javascript”标签 :) 相反,函数声明看起来类似于Haskell代码,我知道很多数学倾向的程序员喜欢。 - גלעד ברקן
@JoshCaswell,就原则而言,我也不同意您的观点。对于标签是否完全匹配,理性的人可能会有不同的看法。而吸引那些可能对紧密相关类别感兴趣的人的注意力,对我来说是完全可以接受的。我的意思是,OP在这里是为了得到问题的答案,对吧?而不是为了服务于一个学究式的编程百科全书项目。 - גלעד ברקן
2
“过时”?与现在主流的语言相比,它更具有未来感。 - molbdnilo
1个回答

3
I think your current understanding is mostly correct and moreover I'm not sure what exactly you don't understand in the else branch. 基本思想和不变量 为了避免名称遮蔽的混淆,让ts0成为传递给polyquotremdts的值,us0=((n,b) :: us)。然后对于quo ts qs,在递归的每一步中都捕获外部上下文中的us0,除了最后一步,即由返回的quo [] qs = (rev qs, [])if m < n then (rev qs, (m,a) :: ts)分支处理的那些步骤,以下不变量成立。
ts0 = us0 * rev qs + ts

换句话说,我们通过递归迭代生成答案(qs)的下一个术语和表示当前除法余数的较低最高幂的新ts。显然,如果我们可以通过此过程减少ts的最高幂,使其最高幂m小于us0的最高幂n,那么我们在qsts中就有完整的答案。现在我们需要做的就是实际减少ts的最高幂。我们将得到的是标准的long division算法。

长除法算法细节

长除法的一步如下:

  1. 对于被除数(当前余数)的最高项 ((m,a) :: ts) 和除数 ((n,b) :: us),即对于 (m,a)(n,b),检查是否 m < n。如果是,则结束算法。

  2. (m,a) 除以 (n,b) 并找到它们的商。显然,它为 (m-n, -a/b)

  3. 用这个商乘以整个除数,即 (map (termproduct(m-n, a/b)) ((n,b) :: us)))。请注意,这不是您代码中的行。稍等一下,我们也会到那里。

旁注 或者如何在代码中实现。以防不清楚:map 是一个经典的高阶函数,它接受另一个函数和一个集合,并返回一个相同大小的新集合,其中每个元素是该函数应用于相应源元素的结果。现在 (termproduct(m-n, a/b)) 被称为 部分应用:它接受两个参数的函数 termproduct 并产生一个新的只有一个参数(原始函数中的第二个参数)的函数,并将第一个参数固定为 (m-n, a/b) 的值。因此,整个构造读作:将 ((n,b) :: us)(或你的代码中的 us)的每个项乘以 (m-n, a/b)。显然,这只是将多项式乘以单项的一种方法。
4. 从当前的余数中减去这个积。我们可以通过在前一步骤上额外乘以 -1 来将这个减法替换为加法。因此现在它是:
(polysum ((m,a) :: ts)
                (map (termproduct(m-n, ~a/b)) ((n,b) :: us)))

现在我们可以注意到最高项实际上会相互抵消。这并不奇怪,因为我们计算乘数时是将它们作为商,以便它们能够相互抵消。所以现在我们可以删除它们,并用你代码中的那一行替换它们:
(polysum ts
                (map (termproduct(m-n, ~a/b)) us))
  1. 最后,我们需要将商/乘数 (m-n, a/b) 添加到我们的结果多项式中。唯一的小问题是我们要按相反的顺序填写,即先找到最高的项,接下来是次高的项,以此类推。这在纸上可以正常工作,但在ML列表中却是按相反顺序填充。这就是为什么我们的“退出”分支都包含rev qs

如果您想要一个例子-您可以在我上面引用的Polynomial long division维基文章中跟随其中一个。再次强调:该算法的每个步骤都是另一个quo的递归调用。

希望这有所帮助。如果仍然不清楚,请告诉我。

更新(回答评论)

我不完全理解数学上 ~a/b 如何互相抵消。

首先,您是否尝试按照我上面提到的维基百科示例进行操作?如果是,那么有什么不清楚的地方吗?

回到您的问题,有几种方法可以考虑。从纯机械的角度来看,问题在于: termproduct(m-n, ~a/b) (n,b)的结果是什么?显然,它是(m-n + n, ~a/b*b)=(m, -a)。因此,这恰好与ts中最高项相反。

从算法的高层级来看,它们将完全抵消,因为我们计算乘数(m-n, ~a/b)的唯一目的是使它们抵消。看起来你没有理解我在“基本思想和不变量”部分所描述的内容,我不确定我能否用不同的方式重申,但我会尝试。除法可以被视为多次进行减法,并计数我们做了多少次。如果我们这样看,很明显我们应该尝试以某种方式减去除数的最高项以消除它。
例1:假设我们正在将2*x^2 + 3*x + 4除以x^2 + x + 1。如果我们想要去掉2*x^2项,我们必须将除数乘以2然后再减去。所以结果是商=2,余数是2*x^2 + 3*x + 4 - 2*(x^2 + x + 1) = x + 2。
例子2:假设我们要将2*x^2 + 3*x + 4除以x^2 + 2*x + 3。请注意,虽然除数与前面的例子不同,但我们仍然必须将其乘以2以消除被除数中的最高项。这是因为唯一确定此乘积的是最高项的比率。显然,其他项会影响最终结果(余数),但它们不会影响结果的当前项。请注意,结果仍为商=2,但余数为2*x^2 + 3*x + 4 - 2*(x^2 + 2*x + 3)=-x - 2
例子3:让我们把2*x^3+3*x^2除以x+1x+2,看看区别。请注意,为了消去x^3,我们在两种情况下的第一个乘数(即商中最高项)应该是相同的2*x^3/x=2*x^2。因此对于x+1,我们得到
x+1: 2*x^3+3*x^2 = 2*x^2*(x+1) + (2*x^3+3*x^2) - 2*x^2*(x+1) = 2*x^2*(x+1) + x^2

同样对于x+2:

x+2: 2*x^3+3*x^2 = 2*x^2*(x+2) + (2*x^3+3*x^2) - 2*x^2*(x+2) = 2*x^2*(x+2) - x^2

从现在开始,由于我们继续使用不同的中间结果 +x^2 代替 x+1-x^2 代替 x+2,所以结果将会有所不同。这意味着下一个乘数将是 +x^2/x = +x-x^2/x = -x
x+1: 2*x^3+3*x^2 = (2*x^2+x)*(x+1) + x^2 - x*(x+1) = (2*x^2+x)*(x+1) - x
x+2: 2*x^3+3*x^2 = (2*x^2-x)*(x+2) - x^2 - (-x)*(x+2) = (2*x^2-x)*(x+2) + 2*x

现在进行最后一步,对于 x+1 进行休息,乘数为 -1,对于 x+2 进行休息,乘数为 2

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

再次可以看到,除数中非最高项的更改会影响结果,但不会影响结果的最高项。

谢谢回复!但是,我不完全理解数学上 ~a/b 如何相互抵消的部分。我一直以为在长除法中,我们使用整个除数来除方程,例如 $x^2+1$,但算法首先使用 $x^2然后再使用1`。数学上,这是如何运作的?我认为我缺少了数学概念,无法完全理解该算法。 - oldselflearner1959
@oldselflearner1959,我尝试在我的答案更新中回答了您的评论。如果仍然不清楚,您可能需要更详细地澄清哪些方面不清楚。 - SergGr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接