F# - Facebook黑客杯 - 双人广场

时间:2011-01-13 14:59:15

标签: algorithm optimization f#

我正在努力加强我的F#-fu并决定解决Facebook黑客杯双方问题。我在运行时遇到了一些问题,并且想知道是否有人可以帮我弄清楚为什么它比我的C#等价物慢得多。

另一篇文章有​​一个很好的描述;

  

来源:Facebook黑客杯   2011年资格赛

     

双平方数是整数X.   这可以表示为。的总和   两个完美的广场。例如,10   是一个双平方因为10 = 3 ^ 2 +   1 ^ 2。给定X,我们如何确定它的方式数量   写成两个正方形的总和?对于   例如,10只能写成3 ^ 2   + 1 ^ 2(我们不计算1 ^ 2 + 3 ^ 2不同)。另一方面,25可以   写成5 ^ 2 + 0 ^ 2或4 ^ 2 + 3 ^ 2.

     

你需要解决这个问题0≤   X≤2,147,483,647。

     

示例:

     

10 => 1

     

25 => 2

     

3 => 0

     

0 => 1

     

1 => 1

     

竞争中的数字

     

1740798996
  1257431873个
  2147483643个
  602519112个
  858320077个
  1048039120个
  415485223个
  874566596个
  1022907856个
  65个
  421330820个
  1041493518个
  5
  1328649093个
  1941554117个
  4225
  2082925个
  0
  1
  3

我的基本策略(我愿意批评)是;

  1. 创建一个初始化为0
  2. 的输入数字的字典(用于memoize)
  3. 获取最大数字(LN)并将其传递给计数/备忘录功能
  4. 获取LN square root as int
  5. 计算所有数字0到LN的平方并存储在dict中
  6. 从0到LN的非重复数字组合的求和方
    • 如果sum在备忘录中,请在备忘录中添加1
  7. 最后,输出原始数字的计数。
  8. 这是F#代码(请参阅底部的代码更改)我写过我相信这个策略对应(运行时间:~8:10);

    open System
    open System.Collections.Generic
    open System.IO
    
    /// Get a sequence of values
    let rec range min max = 
        seq { for num in [min .. max] do yield num }
    
    /// Get a sequence starting from 0 and going to max
    let rec zeroRange max = range 0 max    
    
    /// Find the maximum number in a list with a starting accumulator (acc)
    let rec maxNum acc = function
        | [] -> acc
        | p::tail when p > acc -> maxNum p tail
        | p::tail -> maxNum acc tail
    
    /// A helper for finding max that sets the accumulator to 0
    let rec findMax nums = maxNum 0 nums
    
    /// Build a collection of combinations; ie [1,2,3] = (1,1), (1,2), (1,3), (2,2), (2,3), (3,3)
    let rec combos range =    
        seq { 
              let count = ref 0
              for inner in range do 
                  for outer in Seq.skip !count range do 
                      yield (inner, outer)
                  count := !count + 1          
            }
    
    let rec squares nums = 
        let dict = new Dictionary<int, int>()
        for s in nums do
            dict.[s] <- (s * s)
        dict
    
    /// Counts the number of possible double squares for a given number and keeps track of other counts that are provided in the memo dict.
    let rec countDoubleSquares (num: int) (memo: Dictionary<int, int>) =
        // The highest relevent square is the square root because it squared plus 0 squared is the top most possibility
        let maxSquare = System.Math.Sqrt((float)num)
    
        // Our relevant squares are 0 to the highest possible square; note the cast to int which shouldn't hurt.
        let relSquares = range 0 ((int)maxSquare)
    
        // calculate the squares up front;
        let calcSquares = squares relSquares
    
        // Build up our square combinations; ie [1,2,3] = (1,1), (1,2), (1,3), (2,2), (2,3), (3,3)
        for (sq1, sq2) in combos relSquares do
            let v = calcSquares.[sq1] + calcSquares.[sq2]
            // Memoize our relevant results
            if memo.ContainsKey(v) then            
                memo.[v] <- memo.[v] + 1
    
        // return our count for the num passed in
        memo.[num]
    
    
    // Read our numbers from file.
    //let lines = File.ReadAllLines("test2.txt")
    //let nums = [ for line in Seq.skip 1 lines -> Int32.Parse(line) ]
    
    // Optionally, read them from straight array
    let nums = [1740798996; 1257431873; 2147483643; 602519112; 858320077; 1048039120; 415485223; 874566596; 1022907856; 65; 421330820; 1041493518; 5; 1328649093; 1941554117; 4225; 2082925; 0; 1; 3]
    
    // Initialize our memoize dictionary
    let memo = new Dictionary<int, int>()
    for num in nums do
        memo.[num] <- 0
    
    // Get the largest number in our set, all other numbers will be memoized along the way
    let maxN = findMax nums
    
    // Do the memoize
    let maxCount = countDoubleSquares maxN memo
    
    // Output our results.
    for num in nums do
        printfn "%i" memo.[num]
    
    // Have a little pause for when we debug
    let line = Console.Read()
    

    这是我在C#中的版本(运行时间:~1:40:

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO;
    using System.Linq;
    using System.Text;
    
    namespace FBHack_DoubleSquares
    {
        public class TestInput
        {
            public int NumCases { get; set; }
            public List<int> Nums { get; set; }
    
            public TestInput()
            {
                Nums = new List<int>();
            }
    
            public int MaxNum()
            {
                return Nums.Max();
            }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                // Read input from file.
                //TestInput input = ReadTestInput("live.txt");
    
                // As example, load straight.
                TestInput input = new TestInput
                {
                    NumCases = 20,
                    Nums = new List<int>
                    {
                        1740798996,
                        1257431873,
                        2147483643,
                        602519112,
                        858320077,
                        1048039120,
                        415485223,
                        874566596,
                        1022907856,
                        65,
                        421330820,
                        1041493518,
                        5,
                        1328649093,
                        1941554117,
                        4225,
                        2082925,
                        0,
                        1,
                        3,
                    }
                };
    
                var maxNum = input.MaxNum();
    
                Dictionary<int, int> memo = new Dictionary<int, int>();
                foreach (var num in input.Nums)
                {
                    if (!memo.ContainsKey(num))
                        memo.Add(num, 0);
                }
    
                DoMemoize(maxNum, memo);
    
                StringBuilder sb = new StringBuilder();
                foreach (var num in input.Nums)
                {
                    //Console.WriteLine(memo[num]);
                    sb.AppendLine(memo[num].ToString());
                }
    
                Console.Write(sb.ToString());
    
                var blah = Console.Read();
                //File.WriteAllText("out.txt", sb.ToString());
            }
    
            private static int DoMemoize(int num, Dictionary<int, int> memo)
            {
                var highSquare = (int)Math.Floor(Math.Sqrt(num));
    
                var squares = CreateSquareLookup(highSquare);
                var relSquares = squares.Keys.ToList();
    
                Debug.WriteLine("Starting - " + num.ToString());
                Debug.WriteLine("RelSquares.Count = {0}", relSquares.Count);
    
                int sum = 0;
                var index = 0;            
                foreach (var square in relSquares)
                {
                    foreach (var inner in relSquares.Skip(index))
                    {
                        sum = squares[square] + squares[inner];
                        if (memo.ContainsKey(sum))
                            memo[sum]++;
                    }
                    index++;
                }
    
                if (memo.ContainsKey(num))
                    return memo[num];
    
                return 0;            
            }
    
            private static TestInput ReadTestInput(string fileName)
            {
                var lines = File.ReadAllLines(fileName);
                var input = new TestInput();
                input.NumCases = int.Parse(lines[0]);
                foreach (var lin in lines.Skip(1))
                {
                    input.Nums.Add(int.Parse(lin));
                }
    
                return input;
            }
    
            public static Dictionary<int, int> CreateSquareLookup(int maxNum)
            {
                var dict = new Dictionary<int, int>();
                int square;
                foreach (var num in Enumerable.Range(0, maxNum))
                {
                    square = num * num;
                    dict[num] = square;
                }
    
                return dict;
            }
        }   
    }
    

    谢谢你看看。

    更新

    稍微更改组合功能将导致相当大的性能提升(从8分钟到3:45):

    /// Old and Busted...
    let rec combosOld range =    
        seq { 
              let rangeCache = Seq.cache range
              let count = ref 0
              for inner in rangeCache do 
                  for outer in Seq.skip !count rangeCache do 
                      yield (inner, outer)
                  count := !count + 1          
            }
    
    /// The New Hotness...
    let rec combos maxNum =    
        seq {
            for i in 0..maxNum do
                for j in i..maxNum do
                    yield i,j } 
    

8 个答案:

答案 0 :(得分:7)

同样,x ^ 2 + y ^ 2 = k的整数解的数量是

  • 零,如果有一个素数因子等于3 mod 4
  • k的素数除数的四倍,等于1 mod 4.

请注意,在第二种选择中,您将^ 2 + b ^ 2计算为(-a)^ 2 + b ^ 2(和其他符号)的不同解,并计算为b ^ 2 + a ^ 2。因此,如果你想将解决方案作为集合而不是有序对,那么你可能想要除以4,然后再除以2(如@Wei Hu指出的那样发言)。

了解这一点,编写一个提供解决方案数量的程序很简单:一次性计算最多46341的素数。

给定k,使用上面的列表计算k的素数除数(测试到sqrt(k))。计算等于1 mod 4的数,并求和。如果需要,将4乘以答案。

所有这些都是任何懒惰函数语言中的一两个线程(我不知道f#,在Haskell中它将是两行长),一旦你有一个primes无限序列:计算除数= 1 mod 4(filterby |> count或这些行中的某些东西)是非常自然的东西。

我怀疑它比强制分解更快。

答案 1 :(得分:5)

你的F#combos功能很糟糕。像

这样的东西
let rec combos range =
    let a = range |> Seq.toArray
    seq {
        for i in 0..a.Length-1 do
            for j in i..a.Length-1 do
                yield i,j } 

应该是一个很大的加速。

答案 2 :(得分:2)

我喜欢序列,但在这种情况下,它们可能是错误的工具。这个问题是尝试一个非平凡的递归解决方案的机会。使用变异,很容易做到这样的算法(在Python中,我选择的伪代码......)

def f(n):
    i = 0
    j = int(1 + sqrt(n))
    count = 0
    # 'i' will always be increased, and j will always be decreased.  We
    # will stop if i > j, so we can avoid duplicate pairs.
    while i <= j:
        s = i * i + j * j
        if s < n:
            # if any answers exist for this i, they were with higher
            # j values.  So, increment i.
            i +=  1
        elif s > n:
            # likewise, if there was an answer with this j, it was
            # found with a smaller i.  so, decrement it.
            j -= 1
        else:
            # found a solution.  Count it, and then move both i and 
            # j, because they will be found in at most one solution pair.
            count += 1
            i += 1
            j -= 1
    return count    

现在,这似乎有效。也许它不对,或者不是最好的,但我喜欢递归代码在F#中的样子。 (警告..我这台电脑上没有F#......但我希望我做对了。)

let f n = 
    let rec go i j count =
        if i > j then count
        else
            let s = i * i + j * j
            if s < n then 
                go (i + 1) j count
            else if s > n then
                go i (j - 1) count
            else
                go (i + 1) (j - 1) (count + 1)
    go 0 (1 + (n |> float |> sqrt |> int)) 0

此解决方案在每次调用的O(sqrt(N))时间内运行,并且需要恒定的内存。记忆解决方案花费O(N)时间来设置字典,字典大小至少为O(sqrt(N))。对于大N,这些是完全不同的。

答案 3 :(得分:1)

修改

鉴于C#代码,最大的区别是C#代码循环遍历列表,而F#代码迭代序列。当您实际对seq进行计算时,Seq.cache实际上只是一个帮助,在这种情况下,它不会避免重复遍历序列。

这个函数更像是C#代码,但它要求输入是一个数组,而不仅仅是任何序列。

但是,正如你在其他地方所说,整体上需要更好的算法。

let combos (ss: int []) =
    let rec helper idx =
    seq {
        if idx < ss.Length then
            for jdx in idx + 1 .. ss.Length - 1 do
                 yield (ss.[idx], ss.[jdx])
            yield! helper (idx + 1)
        }
    helper 0

结束编辑

这可能是为什么这比同等的C#代码慢的原因之一,尽管我认为IEnumerable会有类似的问题。

let rec combos range =    
seq { 
      let count = ref 0
      for inner in range do 
          for outer in Seq.skip !count range do 
              yield (inner, outer)
          count := !count + 1          
    }

双环超范围导致它被反复评估。如果必须反复查看序列,可以使用Seq.cache来避免这种情况。

答案 4 :(得分:1)

这应该有所帮助。

// Build up our square combinations;
for (sq1, sq2) in combos maxSquare do
    let v = calcSquares.[sq1] + calcSquares.[sq2]

    // Memoize our relevant results
    match memo.TryGetValue v with
    | true, value -> memo.[v] <- value + 1
    | _ -> ()

答案 5 :(得分:1)

我一直在使用F#进行Facebook黑客攻击。这是我的解决方案,在不到0.5秒的时间内完成。虽然我同意你应该尽可能地发挥功能,但有时必须采取更有效的方法,特别是在时间有限的编码竞赛中。

let int_sqrt (n:int) :int =
    int (sqrt (float n))

let double_square (x: int) :int =
    let mutable count = 0
    let square_x = int_sqrt x
    for i in 0..square_x do
        let y = int_sqrt (x - i*i)
        if y*y + i*i = x && i<=y then count <- count + 1
    count

答案 6 :(得分:0)

不检查代码,您的算法是次优的。我用以下方法解决了这个问题:

Let N be the number we're testing.
Let X,Y such that X^2 + Y^2 = N
For all 0 <= X < sqrt(N)
   Let Ysquared = N - X^2
   if( Ysquared > Xsquared ) break; //prevent duplicate solutions
   if( isInteger( sqrt(ySquared) ) )
      //count unique solution

答案 7 :(得分:0)

  

这是Stefan Kendall答案的改进。   考虑:N = a ^ 2 + b ^ 2且a> = b> = 0   然后:实数中的sqrt(N / 2)&lt; = a&lt; = sqrt(N)。得到一个整数   interval必须在第一个项(sqrt(N / 2))和舍入后四舍五入   在最后一个学期(sqrt(N))。

public class test001 {
    public static void main(String[] args) {
        int k=0,min=0,max=0;
        double[] list={1740798996,1257431873,2147483643,
        602519112,858320077,1048039120,415485223,874566596,
        1022907856,65,421330820,1041493518,5,1328649093,
        1941554117,4225,2082925,0,1,3};
        for(int i=0 ; i<=list.length-1 ; i++){
            if (Math.sqrt(list[i]/2)%1==0)
                min=(int)Math.sqrt(list[i]/2);
            else
                min=(int)(Math.sqrt(list[i]/2))+1;
                    //Rounded up
            max=(int)Math.sqrt(list[i]);
                    //Rounded down
            if(max>=min)
                for(int j=min ; j<=max ; j++)
                if(Math.sqrt(list[i]-j*j)%1==0) k++;
                //If sqrt(N-j^2) is an integer then "k" increases.
        System.out.println((int)list[i]+": "+k);
        k=0; 
        }
    }
}