合格 Haskell 列表理解的有效替代方案
Efficient alternatives to qualified Haskell list comprehensions
作为 Haskell 中合格列表推导的说明,Learn You a Haskell 教程提供了一个列表推导示例,该示例建议使用给定周长找到直角三角形的一般方法 p
,表示为元组:
λ> let rightTriangles p = [ (a,b,c) | c <- [1..(quot p 2)], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2, a + b + c == p]
但是随着p
变大,这种方法变得非常慢。
是否有通常更快但惯用的 Haskell 方法来为大型 p
完成同样的事情?
评论很好地说明了您真正需要的是更好的算法。
但是让我们尝试一些不同的东西,看看我们可以对当前代码进行哪些优化:
let rightTrianglesCubic p = [ (a,b,c) | c <- [1..quot p 2], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2, a + b + c == p]
首先,请注意我们如何遍历 [1..b]
的所有值,直到找到 a + b + c == p
的值。但唯一成立的值是 a = p - b - c
,因此我们可以完全跳过循环,并将其变成二次算法:
let rightTrianglesQuadraticA p = [ (a,b,c) | c <- [1..quot p 2], b <- [1..c], let a = p - b - c, a^2 + b^2 == c^2]
这种方法有一个小问题:
λ rightTrianglesCubic 120
[(30,40,50),(24,45,51),(20,48,52)]
λ rightTrianglesQuadraticA 120
[(40,30,50),(30,40,50),(45,24,51),(24,45,51),(48,20,52),(20,48,52),(0,60,60)]
我们得到了一些额外的结果!这是因为我们忽略了a <- [1..b]
的隐含条件,即1 <= a
和a <= b
。所以让我们把它们加回去。
let rightTrianglesQuadraticB p = [ (a,b,c) | c <- [1..quot p 2], b <- [1..c], let a = p - b - c, a^2 + b^2 == c^2, 1 <= a, a <= b]
现在我们得到了正确答案:
λ rightTrianglesQuadraticB 120
[(30,40,50),(24,45,51),(20,48,52)]
由于 b
的每个值都有唯一的 a
,因此条件 1 <= a
和 a <= b
可以表述为 b
上的条件。 1 <= a
与 1 <= p - b - c
相同或
b <= p - c - 1
。 a <= b
与 p - b - c <= b
或 p - c <= 2*b
相同或
div (p - c + 1) 2 <= b
.
这让我们可以缩小循环的边界 b <- [1..c]
:
let rightTrianglesQuadraticC p = [ (a,b,c) | c <- [1..quot p 2], b <- [max 1 (div (p - c + 1) 2) .. min c (p - c - 1) ], let a = p - b - c, a^2 + b^2 == c^2]
我们甚至可以缩小 c <- [1..quot p 2]
的界限,注意为了 a < b < c
和 a+b+c == p
,我们必须有 c > p/3
:
let rightTrianglesQuadraticD p = [ (a,b,c) | c <- [1 + quot p 3..quot p 2], b <- [max 1 (div (p - c + 1) 2) .. min c (p - c - 1) ], let a = p - b - c, a^2 + b^2 == c^2]
这就是优化此特定代码所能达到的程度。为了进一步提高性能,我们需要完全切换算法。
作为 Haskell 中合格列表推导的说明,Learn You a Haskell 教程提供了一个列表推导示例,该示例建议使用给定周长找到直角三角形的一般方法 p
,表示为元组:
λ> let rightTriangles p = [ (a,b,c) | c <- [1..(quot p 2)], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2, a + b + c == p]
但是随着p
变大,这种方法变得非常慢。
是否有通常更快但惯用的 Haskell 方法来为大型 p
完成同样的事情?
评论很好地说明了您真正需要的是更好的算法。
但是让我们尝试一些不同的东西,看看我们可以对当前代码进行哪些优化:
let rightTrianglesCubic p = [ (a,b,c) | c <- [1..quot p 2], b <- [1..c], a <- [1..b], a^2 + b^2 == c^2, a + b + c == p]
首先,请注意我们如何遍历 [1..b]
的所有值,直到找到 a + b + c == p
的值。但唯一成立的值是 a = p - b - c
,因此我们可以完全跳过循环,并将其变成二次算法:
let rightTrianglesQuadraticA p = [ (a,b,c) | c <- [1..quot p 2], b <- [1..c], let a = p - b - c, a^2 + b^2 == c^2]
这种方法有一个小问题:
λ rightTrianglesCubic 120
[(30,40,50),(24,45,51),(20,48,52)]
λ rightTrianglesQuadraticA 120
[(40,30,50),(30,40,50),(45,24,51),(24,45,51),(48,20,52),(20,48,52),(0,60,60)]
我们得到了一些额外的结果!这是因为我们忽略了a <- [1..b]
的隐含条件,即1 <= a
和a <= b
。所以让我们把它们加回去。
let rightTrianglesQuadraticB p = [ (a,b,c) | c <- [1..quot p 2], b <- [1..c], let a = p - b - c, a^2 + b^2 == c^2, 1 <= a, a <= b]
现在我们得到了正确答案:
λ rightTrianglesQuadraticB 120
[(30,40,50),(24,45,51),(20,48,52)]
由于 b
的每个值都有唯一的 a
,因此条件 1 <= a
和 a <= b
可以表述为 b
上的条件。 1 <= a
与 1 <= p - b - c
相同或
b <= p - c - 1
。 a <= b
与 p - b - c <= b
或 p - c <= 2*b
相同或
div (p - c + 1) 2 <= b
.
这让我们可以缩小循环的边界 b <- [1..c]
:
let rightTrianglesQuadraticC p = [ (a,b,c) | c <- [1..quot p 2], b <- [max 1 (div (p - c + 1) 2) .. min c (p - c - 1) ], let a = p - b - c, a^2 + b^2 == c^2]
我们甚至可以缩小 c <- [1..quot p 2]
的界限,注意为了 a < b < c
和 a+b+c == p
,我们必须有 c > p/3
:
let rightTrianglesQuadraticD p = [ (a,b,c) | c <- [1 + quot p 3..quot p 2], b <- [max 1 (div (p - c + 1) 2) .. min c (p - c - 1) ], let a = p - b - c, a^2 + b^2 == c^2]
这就是优化此特定代码所能达到的程度。为了进一步提高性能,我们需要完全切换算法。