使用从Haskell中的相同列表中提取的参数列出理解

时间:2018-01-09 16:51:03

标签: haskell

我对Haskell中的列表推导有疑问。

我本周晚些时候参加了考试,因此我做了一些旧考试,在那里我找到了这个问题:

“写一个给定正整数n的函数返回一个正整数m≤n的列表,这样就有两个正整数x和y,这样x ^ 2 + y ^ 3 = m。列表需要是排序“

有两个可能的答案,

squareCube::Int->[Int]
squareCube n =[a|a<-[1..n],x<-[1..n],y<-[1..n],x^2+y^3==a]

import Data.List
squareCube::Int->[Int]
squareCube n =
sort [a|x<-[1..n],y<-[1..n],a<-[1..n],x^2+y^3==a]

我想知道为什么在理解中x和y之后需要使用sort函数。为什么参数之间的顺序很重要?

4 个答案:

答案 0 :(得分:4)

此列表已排序:

    [ 1, 1, 1
    , 2, 2, 2
    , 3, 3, 3
    , 4, 4, 4 ]

这个不是:

    [ 1, 2, 3, 4
    , 1, 2, 3, 4
    , 1, 2, 3, 4 ]

答案 1 :(得分:1)

这与这个问题只是模糊地相关:它解决了编程挑战,但没有回答有关现有方法为何有效的问题。但是避免写一个关于它的片段太有趣了,所以就这样了。

通过适当的导入,您甚至可以非常高效地生成方形立方体和的无限列表。基本思想是制作无限列表的无限列表;我们将保持外部无限列表按内部无限列表的头部排序的不变量。然后合并所有这些是简单而有效的。使用the appropriate package,它是一个单行,并且非常简洁地匹配问题描述:

import Data.List.Ordered
squareCubes = unionAll [[x^2+y^3 | x <- [1..]] | y <- [1..]]

我们可以将其效率与现有的两种方法进行比较。这是我用-O2编译的测试程序:

import Data.List
import Data.List.Ordered
import System.Environment

squareCubes = unionAll [[x^2+y^3 | x <- [1..]] | y <- [1..]]
squareCube n = takeWhile (<=n) squareCubes
squareCube' n = [a|a<-[1..n],x<-[1..n],y<-[1..n],x^2+y^3==a]
squareCube'' n = sort [a|x<-[1..n],y<-[1..n],a<-[1..n],x^2+y^3==a]

main = do
    [kind, limit] <- getArgs
    let f = case kind of
            "inf" -> squareCube
            "unsorted" -> squareCube'
            "sorted" -> squareCube''
    print . sum . f . read $ limit

以下是时间,确实很明显:

% /usr/bin/time ./test unsorted 700  
57465
9.60user 0.01system 0:09.63elapsed 99%CPU (0avgtext+0avgdata 4156maxresident)k
% /usr/bin/time ./test sorted 700 
57465
1.87user 0.00system 0:01.87elapsed 99%CPU (0avgtext+0avgdata 4056maxresident)k
% /usr/bin/time ./test inf 700   
50895
0.00user 0.00system 0:00.00elapsed 100%CPU (0avgtext+0avgdata 3616maxresident)k

其他人需要几秒钟(计算机时间的时间),而在某些方面比其他人更有能力的人甚至不会在计时器上注册!我还试验了在达到其他两个实现的时间之前我们可以给出多大的输入。我发现输入500000000需要8.88秒 - 几乎在同一时间内高出近六个数量级。

等等,你说:那些输出是不同的。什么给出了什么?好吧,事实证明,缓慢的实现具有我认为的错误:如果有多种方法将它构造为正方形和立方体的总和,它们将多次吐出一个数字。例如,

> squareCube' 17
[2,5,9,10,12,17,17]
> squareCube 17
[2,5,9,10,12,17]

因为3 ^ 2 + 2 ^ 3 = 4 ^ 2 + 1 ^ 3。另一方面,如果这是预期的行为,可以通过将unionAll替换为mergeAll,在高效,懒惰的单行中轻松实现。

答案 2 :(得分:0)

要清楚地了解列表推导的内容,请尝试

do { print [  (x,[10..13])           | x <- [1,2]]
   ; print [ [(x,y) | y <- [10..13]] | x <- [1,2]]
   ; print [ r                       | x <- [1,2], r <- [(x,y) | y <- [10..13]]]
   ; print [  (x,y)                  | x <- [1,2],               y <- [10..13] ] 
   }
=>
[ (1,[10,11,12,13]),             (2,[10,11,12,13])           ]
[[(1,10),(1,11),(1,12),(1,13)], [(2,10),(2,11),(2,12),(2,13)]]
[ (1,10),(1,11),(1,12),(1,13),   (2,10),(2,11),(2,12),(2,13) ]
[ (1,10),(1,11),(1,12),(1,13),   (2,10),(2,11),(2,12),(2,13) ]

另一方面,

do { print [  ([1,2],y)              | y <- [10..13] ]
   ; print [  (x,y)                  | y <- [10..13], x <- [1,2] ]
   }
=>
[ ([1,2],10),    ([1,2],11),    ([1,2],12),    ([1,2],13)    ]
[ (1,10),(2,10), (1,11),(2,11), (1,12),(2,12), (1,13),(2,13) ]

列表推导以嵌套方式工作。

您可以将第一个代码重写为

map (\(a,_,_) -> a) $
  filter (\(a,x,y) -> x^2+y^3==a) $
    liftA3 (,,) [1..n] [1..n] [1..n]     -- (1)

和第二个

map (\(_,_,a) -> a) $
  filter (\(x,y,a) -> x^2+y^3==a) $
    liftA3 (,,) [1..n] [1..n] [1..n]     -- (2)

您可能很想看到(1)(2)是一个非常笼统的“从同一个列表中抽取的三个元素的所有组合”。但是Haskell是一种确定性语言。它为相同的输入产生相同的结果。因此,它在结果列表上强制执行某个顺序。

当我们说列表推导以嵌套的方式工作时,这意味着什么 - 最左边的列表元素在最终组合中变化最慢,最右边是最快的 - 就像在里程表中一样。

要解决您的问题,您可以写

sort [a | x2 <- takeWhile (<= n) [x^2    | x <- [1..]]
        , a  <- takeWhile (<= n) [x2+y^3 | y <- [1..]] ]

但这需要仔细考虑,代码并不能清楚地表达原始意图,并且仍然不如使用Data.List.Ordered.mergeAll(来自data-ordlist包)那样最优化Daniel Wagner回答,

takeWhile (<= n) . mergeAll $ [[x^2+y^3 | x <- [1..]] | y <- [1..]]

尽管两者具有相同的时间复杂度,但或多或​​少。 mergeAll通过使用排列在树slanted to the right中的成对合并来合并它呈现的有序非递减列表。

想想看,我们也可以写出更自然的

   sort . concat . map (takeWhile (<= n))   
                 $ [[x^2+y^3 | x <- [1..n]] | y <- [1..n]]

这不适用于无限的列表列表。要解决这个问题,我们可以写

-- sort . concat . takeWhile (not . null) . map (takeWhile (<= n)) 
   sort . concat . map (takeWhile (<= n)) . takeWhile ((<= n).head)
                 $ [[x^2+y^3 | x <- [1..]] | y <- [1..]]

在Haskell中,通常最好不要过于努力地将自己弄清楚,而是将其留给懒惰的评估来处理事情。不幸的是,它并没有完全奏效,我们必须特别注意能够处理无限列表 1 ,这些明确的takeWhile对于任务的逻辑是多余的。

对于任何 n ,确实如此

  under n (union a b)         == nub . sort $ under n a ++ under n b
  under n . unionAll . take m == under n . foldl union [] . take m
  under n . unionAll          == nub . sort . concat 
                                     . takeWhile (not.null) . map (under n)

  under n (merge a b)         == sort $ under n a ++ under n b
  under n . mergeAll . take m == under n . foldl merge [] . take m
  under n . mergeAll          == sort . concat 
                                      . takeWhile (not.null) . map (under n)

使用under n = takeWhile (<= n),有序增加列表。

1 Data.List.Ordered.mergeAll单独处理无限列表,并且在在线的意义上更好 - 它开始生成它输出比我们折磨的构造函数早得多。关键不在于不需要库函数,只是为了看看没有它可以做什么。

答案 3 :(得分:0)

由于评估顺序的原因,axy之后需要排序函数。如果首先调用a <- [1..],则将依次针对每个a评估每个后续语句,因此a已经形成了增加的列表:

a = 1
  x <- [1..n]
    y <- [1..n]
    ...return a if there's a valid match

a = 2
    x <- [1..n]
      y <- [1..n]
      ...return a if there's a valid match

etc.

但是,如果最后评估a <- [1..n],我们可能无法得到a s的有序序列:

  x = 1
    y <- [1..n]
      ...
      y = 1
        a <- [1..n]
        ...
        a = 2                 2
      y = 2
        a <- [1..n]
        ...
        a = 9                 9

  x = 2
    y <- [1..n]
      ...
      y = 1
        a <- [1..n]
        ...
        a = 5                 5