F # - Facebook Hacker Cup - Двойные квадраты - PullRequest
4 голосов
/ 13 января 2011

Я работаю над усилением своего F # -фу и решил заняться проблемой двойных квадратов на Facebook Hacker Cup. У меня возникли некоторые проблемы со временем выполнения, и мне было интересно, может ли кто-нибудь помочь мне понять, почему он намного медленнее, чем мой эквивалент C #.

Есть хорошее описание из другого поста;

Источник: Facebook Hacker Cup Отборочный раунд 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. Получить наибольшее число (LN) и передать его функции подсчета / напоминания
  3. Получите квадратный корень LN как int
  4. Рассчитать квадраты для всех чисел от 0 до LN и сохранить в dict
  5. Сумма квадратов для неповторяющихся комбинаций чисел от 0 до LN
    • Если сумма указана в записке, добавьте 1 к записке
  6. Наконец, выведите количество исходных чисел.

Вот код 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;
        }
    }   
}

Спасибо, что заглянули.

UPDATE

Небольшое изменение функции комбо приведет к довольно значительному увеличению производительности (с 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 ]

7 голосов
/ 13 января 2011

Опять же, число целочисленных решений x ^ 2 + y ^ 2 = k равно либо

  • ноль, если есть простой множитель, равный 3 мод 4
  • в четыре раза превышает число простых делителей k, равных 1 мод 4.

Обратите внимание, что во втором варианте вы считаете a ^ 2 + b ^ 2 как другое решение (-a) ^ 2 + b ^ 2 (и других знаков) и b ^ 2 + a ^ 2. Так что вы можете разделить на четыре и снова разделить на 2 (взяв слово, как указывает @Wei Hu), если вы хотите, чтобы решения представляли собой наборы вместо упорядоченных пар.

Зная это, легко написать программу, которая дает число решений: вычислить простые числа до 46341 раз и навсегда.

Для данного k вычислить простые делители k, используя приведенный выше список (тест до sqrt (k)). Подсчитайте те, которые равны 1 мод 4, и суммируйте. При необходимости умножьте ответ на 4.

Все это одна или две строки в любом ленивом функциональном языке (я не знаю достаточно f #, в Haskell это будет две строки), когда у вас есть primes бесконечная последовательность: подсчет делителей = 1 мод 4 (filterby |> count или что-то в этом роде) - это нечто вполне естественное.

Я подозреваю, что это быстрее, чем грубое форсирование разложения.

5 голосов
/ 13 января 2011

Ваша функция 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 голосов
/ 17 января 2011

Мне нравятся последовательности, но в этом случае они, вероятно, не тот инструмент для работы. Эта проблема - шанс попробовать нетривиальное рекурсивное решение. Используя мутацию, было бы легко сделать алгоритм, подобный этому (в 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 они совершенно разные.

1 голос
/ 27 января 2011

Я использую 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
1 голос
/ 14 января 2011

Это должно помочь некоторым.

// 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
    | _ -> ()
1 голос
/ 13 января 2011

EDIT

Учитывая код C #, самое большое отличие состоит в том, что код C # цикличен по списку, в то время как код F # повторяется по последовательности. Seq.cache действительно полезен только тогда, когда вы фактически выполняете вычисления над seq, в этом случае он не избегает многократного обхода последовательности.

Эта функция больше похожа на код 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 может использоваться, чтобы избежать этого, если последовательность должна просматриваться повторно.

0 голосов
/ 30 января 2013

Это улучшение ответа Стефана Кендалла. Рассмотрим: N = a ^ 2 + b ^ 2 и a> = b> = 0 Тогда: sqrt (N / 2) <= a <= sqrt (N) в реалах. Чтобы получить целое число интервал должен быть округлен до первого слагаемого (sqrt (N / 2)) и округлен на последний член (sqrt (N)). </p>

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; 
        }
    }
}
0 голосов
/ 13 января 2011

Без проверки кода ваш алгоритм неоптимален.Я решил это следующим образом:

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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...