F#中不可变集和映射的样式是什么?

时间:2010-01-10 10:31:51

标签: f# coding-style functional-programming set

我刚刚在Project Euler中解决了problem23,其中我需要一个 set 来存储所有丰富的数字。 F#有一个不可变的集合,我可以使用Set.empty.Add(i)创建一个包含数字i的新集合。但我不知道如何使用不可变集来做更复杂的事情。

例如,在下面的代码中,我需要查看数字'x'是否可以写为集合中两个数字的总和。我采用排序数组和数组的二进制搜索算法来完成工作。

还请评论我对以下程序的风格。谢谢!

let problem23 = 
    let factorSum x =
        let mutable sum = 0
        for i=1 to x/2 do
            if x%i=0 then
                sum <- sum + i
        sum
    let isAbundant x = x < (factorSum x)
    let abuns = {1..28123} |> Seq.filter isAbundant |> Seq.toArray
    let inAbuns x = Array.BinarySearch(abuns, x) >= 0
    let sumable x = 
        abuns |> Seq.exists (fun a -> inAbuns (x-a))
    {1..28123} |> Seq.filter (fun x -> not (sumable x)) |> Seq.sum

更新版本:

let problem23b =
    let factorSum x =
        {1..x/2} |> Seq.filter (fun i->x%i=0) |> Seq.sum
    let isAbundant x = x < (factorSum x)
    let abuns = Set( {1..28123} |> Seq.filter isAbundant )
    let inAbuns x = Set.contains x abuns  
    let sumable x = 
        abuns |> Seq.exists (fun a -> inAbuns (x-a))
    {1..28123} |> Seq.filter (fun x -> not (sumable x)) |> Seq.sum

此版本在大约27秒内运行,而前23秒(我已经运行了几次)。因此,与具有二分搜索的排序数组相比,不可变的红黑树实际上没有太大的速度。集/数组中的元素总数为6965

3 个答案:

答案 0 :(得分:4)

你的风格对我来说很好看。算法中的不同步骤是明确的,这是使某些东西起作用的最重要部分。这也是我用来解决项目欧拉问题的策略。首先让它工作,然后快速。

正如已经提到的,用Set.contains替换Array.BinarySearch使代码更具可读性。我发现在我编写的几乎所有PE解决方案中,我只使用数组进行查找。我发现在F#中使用序列和列表作为数据结构更自然。一旦你习惯了它们,那就是。

我认为在函数内使用可变性并不一定是坏事。我已经将问题155从近3分钟优化到7秒,并进行了一些积极的可变性优化。一般来说,我会将其保存为优化步骤,并开始使用折叠/过滤器等编写它。在问题155的示例情况下,我确实开始使用不可变函数组合,因为它进行了测试,最重要的是,理解,我的方法很简单。

选择错误的算法对解决方案比对先使用稍慢的不可变方法更不利。一个好的算法仍然很快,即使它比可变版本慢(沙发你好队长明显!咳嗽)。

编辑:让我们看看你的版本

你的问题23b()在我的电脑上花了31秒。

优化1:使用新算法。

//useful optimization: if m divides n, (n/m) divides n also
//you now only have to check m up to sqrt(n)
let factorSum2 n = 
    let rec aux acc m =
        match m with
        | m when m*m = n -> acc + m
        | m when m*m > n -> acc
        | m -> aux (acc + (if n%m=0 then m + n/m else 0)) (m+1)
    aux 1 2

这仍然是功能样式,但在代码中使用此更新的factorSum,执行时间从31秒到8秒。

所有东西仍然是不可变的样式,但让我们看看当使用数组查找而不是集合时会发生什么:

优化2:使用数组进行查找:

let absums() = 
    //create abundant numbers as an array for (very) fast lookup
    let abnums = [|1..28128|] |> Array.filter (fun n -> factorSum2 n > n)
    //create a second lookup: 
    //a boolean array where arr.[x] = true means x is a sum of two abundant numbers
    let arr = Array.zeroCreate 28124
    for x in abnums do 
        for y in abnums do
            if x+y<=28123 then arr.[x+y] <- true
    arr

let euler023() = 
    absums() //the array lookup
    |> Seq.mapi (fun i isAbsum -> if isAbsum then 0 else i) //mapi: i is the position in the sequence
    |> Seq.sum

//I always write a test once I've solved a problem.
//In this way, I can easily see if changes to the code breaks stuff.
let test() = euler023() = 4179871 

执行时间:0.22秒(!)。

这就是我非常喜欢F#,它仍然允许你使用可变构造来修改你的算法。但我仍然只是之后我先做了一些更优雅的工作。

答案 1 :(得分:3)

您可以从给定的值序列轻松创建Set

let abuns = Set (seq {1..28123} |> Seq.filter isAbundant)
因此,

inAbuns将被重写为

let inAbuns x = abuns |> Set.mem x

Seq.exists将更改为Set.exists

但阵列实现也很好......

请注意,factorSum中不需要使用可变值,除了它是不正确的,因为你计算除数的而不是它们的总和:

let factorSum x = seq { 1..x/2 } |> Seq.filter (fun i -> x % i = 0) |> Seq.sum

答案 2 :(得分:2)

这是一个简单的功能解决方案,比原来的更短,速度超过100倍:

let problem23 =
  let rec isAbundant i t x =
    if i > x/2 then x < t else
      if x % i = 0 then isAbundant (i+1) (t+i) x else
        isAbundant (i+1) t x
  let xs = Array.Parallel.init 28124 (isAbundant 1 0)
  let ys = Array.mapi (fun i b -> if b then Some i else None) xs |> Array.choose id
  let f x a = x-a < 0 || not xs.[x-a]
  Array.init 28124 (fun x -> if Array.forall (f x) ys then x else 0)
  |> Seq.sum

第一个技巧是记录由数字本身索引的数组中的丰富数字,而不是使用搜索结构。第二个技巧是注意到所有时间都花在生成该数组上,因此要并行执行。