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

发布于 2024-08-17 05:09:00 字数 1421 浏览 2 评论 0原文

我刚刚解决了 Project Euler 中的 problem23 ,其中我需要一个设置来存储所有丰富的数字。 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

I have just solved problem23 in Project Euler, in which I need a set to store all abundant numbers. F# has a immutable set, I can use Set.empty.Add(i) to create a new set containing number i. But I don't know how to use immutable set to do more complicated things.

For example, in the following code, I need to see if a number 'x' could be written as the sum of two numbers in a set. I resort to a sorted array and array's binary search algorithm to get the job done.

Please also comment on my style of the following program. Thanks!

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

the updated version:

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

This version runs in about 27 seconds, while the first 23 seconds(I've run several times). So an immutable red-black tree actually does not have much speed down compared to a sorted array with binary search. The total number of elements in the set/array is 6965.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

晨敛清荷 2024-08-24 05:09:00

你的风格很适合我。算法中的不同步骤是清晰的,这是使某些东西发挥作用的最重要的部分。这也是我用来解决欧拉计划问题的策略。首先使其工作,然后使其快速。

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

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

选择错误的算法比首先使用速度稍慢的不可变方法对解决方案的危害要大得多。一个好的算法即使比可变版本慢,仍然很快(沙发你好队长明显!咳嗽)。

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

您的 Problem23b() 在我的电脑上花了 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# 的原因,它仍然允许您使用可变结构来修补算法的底层。但我仍然只是在先完成一些更优雅的工作之后才这样做。

Your style looks fine to me. The different steps in the algorithm are clear, which is the most important part of making something work. This is also the tactic I use for solving Project Euler problems. First make it work, and then make it fast.

As already remarked, replacing Array.BinarySearch by Set.contains makes the code even more readable. I find that in almost all PE solutions I've written, I only use arrays for lookups. I've found that using sequences and lists as data structures is more natural within F#. Once you get used to them, that is.

I don't think using mutability inside a function is necessarily bad. I've optimized problem 155 from almost 3 minutes down to 7 seconds with some aggressive mutability optimizations. In general though, I'd save that as an optimization step and start out writing it using folds/filters etc. In the example case of problem 155, I did start out using immutable function composition, because it made testing and most importantly, understanding, my approach easy.

Picking the wrong algorithm is much more detrimental to a solution than using a somewhat slower immutable approach first. A good algorithm is still fast even if it's slower than the mutable version (couch hello captain obvious! cough).

Edit: let's look at your version

Your problem23b() took 31 seconds on my PC.

Optimization 1: use new algorithm.

//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

This is still very much in functional style, but using this updated factorSum in your code, the execution time went from 31 seconds to 8 seconds.

Everything's still in immutable style, but let's see what happens when an array lookup is used instead of a set:

Optimization 2: use an array for lookup:

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 

Execution time: 0.22 seconds (!).

This is what I like so much about F#, it still allows you to use mutable constructs to tinker under the hood of your algorithm. But I still only do this after I've made something more elegant work first.

北笙凉宸 2024-08-24 05:09:00

您可以根据给定的值序列轻松创建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

You can easily create a Set from a given sequence of values.

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

inAbuns would therefore be rewritten to

let inAbuns x = abuns |> Set.mem x

Seq.exists would be changed to Set.exists

But the array implementation is fine too ...

Note that there is no need to use mutable values in factorSum, apart from the fact that it's incorrect since you compute the number of divisors instead of their sum:

let factorSum x = seq { 1..x/2 } |> Seq.filter (fun i -> x % i = 0) |> Seq.sum
这样的小城市 2024-08-24 05:09:00

这是一个简单的功能解决方案,它比原来的更短,速度快了 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

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

Here is a simple functional solution that is shorter than your original and over 100× faster:

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

The first trick is to record which numbers are abundant in an array indexed by the number itself rather than using a search structure. The second trick is to notice that all the time is spent generating that array and, therefore, to do it in parallel.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文