我要花一会儿时间,但有一点。
半群
答案是二元归约运算的关联性质。
那是非常抽象的,但是乘法就是一个很好的例子。如果x,y和z是一些自然数(或整数,有理数,实数或复数或N × N矩阵,或更多其他事物),则x × y是同一种x和y的个数。我们从两个数字开始,所以它是一个二进制运算,然后得到一个,因此我们将拥有的数字数量减少了一个,从而使它成为减少运算。并且(x × y)× z始终与x ×(y ×z),这是关联属性。
(如果您已经知道所有这些,则可以跳到下一部分。)
您在计算机科学中经常看到的其他一些事情也以相同的方式起作用:
- 将任何这些数字相加而不是相乘
- 连接字符串(
"a"+"b"+"c"
是"abc"
你是否开始"ab"+"c"
或"a"+"bc"
)
- 将两个列表拼接在一起。
[a]++[b]++[c]
类似地[a,b,c]
从后到前或从前到后。
cons
如果将头视为单例列表,则在头和尾上。那只是串联两个列表。
- 取集合的并集或交集
- 布尔和,布尔或
- 按位
&
,|
和^
- 的功能的组合物:(˚F ∘ 克)∘ ħ X = ˚F ∘(克 ∘ ħ)X = ˚F(克(ħ(X)))
- 最大值和最小值
- 此外模p
有些事情没有:
- 减法,因为1-(1-2)≠(1-1)-2
- X ⊕ ÿ = TAN(X + Ý),因为黄褐色(π/ 4 +π/ 4)是未定义
- 负数相乘,因为-1×-1不是负数
- 整数除法,它具有所有三个问题!
- 逻辑不,因为它只有一个操作数,而不是两个
int print2(int x, int y) { return printf( "%d %d\n", x, y ); }
,as print2( print2(x,y), z );
和print2( x, print2(y,z) );
具有不同的输出。
我们将其命名为一个有用的概念。具有这些属性的操作的集合为准群。因此,乘法下的实数是一个半群。您的问题原来是这种抽象在现实世界中变得有用的方式之一。 半组操作都可以按照您的要求进行优化。
在家尝试一下
据我所知,这种技术最早是在1974年在Daniel Friedman和David Wise的论文“将程式化的递归折叠成迭代”中描述的,尽管他们假定的属性比实际需要的要多。
Haskell是一种很好的语言来说明这一点,因为它的Semigroup
标准库中有typeclass。它把泛型Semigroup
的操作称为运算符<>
。由于列表和字符串是的实例Semigroup
,因此它们的实例定义为例如<>
串联运算符++
。正确导入后,[a] <> [b]
是的别名[a] ++ [b]
,即[a,b]
。
但是,数字呢?我们刚才看到的数字类型是下半群或者另外或乘法!那么哪一个<>
适合Double
?好吧,任一个!Haskell中定义了类型Product Double
,where (<>) = (*)
(即在Haskell实际定义),并且还Sum Double
,where (<>) = (+)
。
一种皱纹是您使用了1是乘法身份的事实。具有身份的半群称为monoid,并在Haskell程序包中定义,该程序包Data.Monoid
调用类型类的泛型身份元素mempty
。 Sum
,Product
以及列表中的每个都有一个标识元件(0,1和[]
分别),因此它们的实例Monoid
以及Semigroup
。(不要与monad混淆,所以请忘记我什至把它们提了出来。)
这些信息足以将您的算法转化为使用半定式的Haskell函数:
module StylizedRec (pow) where
import Data.Monoid as DM
pow :: Monoid a => a -> Word -> a
{- Applies the monoidal operation of the type of x, whatever that is, by
- itself n times. This is already in Haskell as Data.Monoid.mtimes, but
- let’s write it out as an example.
-}
pow _ 0 = mempty -- Special case: Return the nullary product.
pow x 1 = x -- The base case.
pow x n = x <> (pow x (n-1)) -- The recursive case.
重要的是,请注意,这是尾递归模半群:每种情况都是一个值,一个尾递归调用或两者的半群乘积。另外,此示例碰巧mempty
用于其中一种情况,但是如果我们不需要这样做,则可以使用更通用的typeclass来完成Semigroup
。
让我们在GHCI中加载该程序,并查看其工作方式:
*StylizedRec> getProduct $ pow 2 4
16
*StylizedRec> getProduct $ pow 7 2
49
还记得我们宣布pow
了一个通用的Monoid
,其类型我们叫a
?我们给GHCI足够的信息来推断该类型a
在这里Product Integer
,这是一个instance
的Monoid
,其<>
操作整数乘法。因此,pow 2 4
将其递归扩展2<>2<>2<>2
为2*2*2*2
或16
。到目前为止,一切都很好。
但是,我们的函数仅使用通用的monoid操作。之前,我说过还有另一个实例,Monoid
称为Sum
,其<>
操作为+
。我们可以试试吗?
*StylizedRec> getSum $ pow 2 4
8
*StylizedRec> getSum $ pow 7 2
14
现在,相同的扩展给了我们2+2+2+2
而不是2*2*2*2
。乘法就是加法,乘幂就是乘法!
但是我还给出了Haskell monoid的另一个示例:列表,其操作是串联的。
*StylizedRec> pow [2] 4
[2,2,2,2]
*StylizedRec> pow [7] 2
[7,7]
文字[2]
告诉编译器这是一个列表,<>
on上是++
,[2]++[2]++[2]++[2]
也是[2,2,2,2]
。
最后,一个算法(实际上是两个)
通过简单的更换x
用[x]
,将转换为通用的算法,使用递归模半群为一体,创建一个列表。哪个名单? 该算法适用的元素列表<>
。 因为我们仅使用列表具有的半组运算,所以结果列表将与原始计算同构。并且由于原始操作是关联的,因此我们同样可以很好地从后到前或从前到后评估元素。
如果您的算法曾经达到基本情况并终止,则列表将为非空。由于终端案例返回了某些内容,因此它将是列表的最后一个元素,因此它将至少包含一个元素。
如何按顺序对列表的每个元素应用二进制归约运算?是的,折叠。所以,你可以替换[x]
的x
,得到的元素列表,以减少<>
,然后要么对折或左折列表:
*StylizedRec> getProduct $ foldr1 (<>) $ pow [Product 2] 4
16
*StylizedRec> import Data.List
*StylizedRec Data.List> getProduct $ foldl1' (<>) $ pow [Product 2] 4
16
与该版本foldr1
实际上在标准库中存在,如sconcat
对Semigroup
和mconcat
的Monoid
。它在列表上没有偷懒的右折。也就是说,它扩展[Product 2,Product 2,Product 2,Product 2]
为2<>(2<>(2<>(2)))
。
在这种情况下,这样做效率不高,因为在生成所有术语之前,您无法对各个术语进行任何操作。(在这里我曾经讨论过什么时候使用右折以及何时使用严格的左折,但这太离谱了。)
的版本foldl1'
是经过严格评估的左折。也就是说,具有严格累加器的尾递归函数。计算结果为(((2)<>2)<>2)<>2
,立即计算出来,不需要时再计算。(至少,折叠本身没有延迟:要折叠的列表是由另一个可能包含惰性求值的函数在此处生成的。)因此,折叠先计算(4<>2)<>2
,然后立即计算8<>2
,然后再计算16
。这就是为什么我们需要将操作进行关联的原因:我们只是更改了括号的分组!
严格的左折等同于GCC所做的事情。 上一个示例中最左边的数字是累加器,在这种情况下为运行产品。在每个步骤中,它将乘以列表中的下一个数字。表示它的另一种方法是:对要相乘的值进行迭代,将运行乘积保持在累加器中,并且在每次迭代时,将累加器乘以下一个值。也就是说,这是一个while
变相的循环。
有时可以使它同样有效。编译器可能能够优化内存中的列表数据结构。从理论上讲,它在编译时具有足够的信息,可以在此处确定应该这样做:[x]
是单例,因此[x]<>xs
与相同cons x xs
。函数的每次迭代都可以重新使用相同的堆栈框架并在适当位置更新参数。
在特定情况下,右折或严格左折可能更合适,因此请确定要使用哪一种。还有一些事情只有正确折叠才能完成(例如,在不等待所有输入的情况下生成交互式输出,并在无限列表上进行操作)。不过,在这里,我们将一系列操作简化为一个简单的值,因此我们需要严格的左折。
因此,如您所见,可以自动将尾半递归模的任意半组(其中一个例子是乘法下的任何常规数值类型)的模数自动优化为惰性右折或严格左折哈斯克尔。
进一步推广
二进制操作的两个参数不必为同一类型,只要初始值与结果为同一类型即可。(当然,您总是可以翻转参数以匹配您正在执行的折叠的顺序(向左或向右)。)因此,您可能会反复向文件中添加补丁以获取更新的文件,或者以初始值为1.0,除以整数可累计浮点结果。或在空白列表前添加元素以获取列表。
泛化的另一种类型是不将折叠应用于列表,而是应用于其他Foldable
数据结构。通常,不可变的线性链表不是给定算法所需的数据结构。我上面没有提到的一个问题是,将元素添加到列表的前面比后面添加元素要有效得多,并且当操作不是可交换的时,在操作x
的左边和右边都没有应用相同。因此,您将需要使用另一对结构(例如一对列表或二叉树)来表示一种算法,该算法既可以应用于x
右侧也可以应用于<>
左侧。
还要注意,associative属性允许您以其他有用的方式重新组合操作,例如分治法:
times :: Monoid a => a -> Word -> a
times _ 0 = mempty
times x 1 = x
times x n | even n = y <> y
| otherwise = x <> y <> y
where y = times x (n `quot` 2)
或自动并行性,其中每个线程将子范围缩小为一个值,然后将其与其他线程组合。
if(n==0) return 0;
(不像您的问题那样返回1)。x^0 = 1
,所以这是一个错误。不过,这对于其余的问题并不重要;迭代式asm首先检查该特殊情况。但是奇怪的是,迭代实现引入了1 * x
源代码中不存在的乘数,即使我们创建了一个float
版本。 gcc.godbolt.org/z/eqwine(而gcc仅以。成功-ffast-math
)