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