При создании промежуточного значения я должен хранить его? - PullRequest
2 голосов
/ 10 января 2010

Я пытаюсь выучить F #, поэтому я посетил Project Euler , и сейчас я работаю над Задачей 3 .

Основными факторами 13195 являются 5, 7, 13 и 29.

Какой самый большой простейший коэффициент числа 600851475143?

Некоторые вещи, которые следует учитывать:

  1. Мой первый приоритет - выучить хорошие функциональные привычки.
  2. Мой второй приоритет - я хочу, чтобы он был быстрым и эффективным.

В следующем коде я отметил раздел, к которому относится этот вопрос.

let isPrime(n:int64) = 
    let rec check(i:int64) = 
        i > n / 2L or (n % i <> 0L && check(i + 1L))
    check(2L) 

let greatestPrimeFactor(n:int64) =
    let nextPrime(prime:int64):int64 = 
        seq { for i = prime + 1L to System.Int64.MaxValue do if isPrime(i) then yield i }  
        |> Seq.skipWhile(fun v -> n % v <> 0L) 
        |> Seq.hd
    let rec findNextPrimeFactor(number:int64, prime:int64):int64 =
        if number = 1L then prime else 
            //************* No variable
            (fun p -> findNextPrimeFactor(number / p, p))(nextPrime(prime))    
            //*************               
            //************* Variable
            let p = nextPrime(prime)
            findNextPrimeFactor(number / p, p) 
            //*************
    findNextPrimeFactor(n, 2L) 

Обновление

На основании некоторых отзывов я реорганизовал код, чтобы он был в 10 раз быстрее.

module Problem3

module private Internal =
    let execute(number:int64):int64 = 
        let rec isPrime(value:int64, current:int64) = 
            current > value / 2L or (value % current <> 0L && isPrime(value, current + 1L))   
        let rec nextPrime(prime:int64):int64 = 
            if number % prime = 0L && isPrime(prime, 2L) then prime else nextPrime(prime + 1L)       
        let rec greatestPrimeFactor(current:int64, prime:int64):int64 =
            if current = 1L then prime else nextPrime(prime + 1L) |> fun p -> greatestPrimeFactor(current / p, p)
        greatestPrimeFactor(number, 2L)


let execute() = Internal.execute(600851475143L)

Обновление

Я хотел бы поблагодарить всех за совет. Эта последняя версия представляет собой сборник всех советов, которые я получил.

module Problem3

module private Internal =
    let largestPrimeFactor number = 
        let rec isPrime value current = 
            current > value / 2L || (value % current <> 0L && isPrime value (current + 1L))   
        let rec nextPrime value =
            if number % value = 0L && isPrime value 2L then value else nextPrime (value + 1L) 
        let rec find current prime =
            match current / prime with
            | 1L -> prime
            | current -> nextPrime (prime + 1L) |> find current
        find number (nextPrime 2L)            

let execute() = Internal.largestPrimeFactor 600851475143L

Ответы [ 4 ]

7 голосов
/ 10 января 2010

Функциональное программирование становится легче и более автоматическим с практикой, так что не переживайте, если вы не поняли его с первой попытки.

Имея это в виду, давайте возьмем ваш пример кода:

let rec findNextPrimeFactor(number:int64, prime:int64):int64 =
        if number = 1L then prime else 
            //************* No variable
            (fun p -> findNextPrimeFactor(number / p, p))(nextPrime(prime))    
            //*************               
            //************* Variable
            let p = nextPrime(prime)
            findNextPrimeFactor(number / p, p) 
            //*************

Ваша no variable версия просто странная, не используйте ее. Мне нравится ваша версия с явной привязкой let.

Другой способ написать это будет:

nextPrime(prime) |> fun p -> findNextPrimeFactor(number / p, p)

Это хорошо и иногда полезно написать это так, но все же выглядит немного странно. В большинстве случаев мы используем |> для каррирования значений без необходимости именовать наши переменные (в стиле "pointfree"). Постарайтесь предвидеть, как будет использоваться ваша функция, и, если возможно, переписать ее, чтобы вы могли использовать ее с оператором канала без явных объявленных переменных. Например:

let rec findNextPrimeFactor number prime =
    match number / prime with
    | 1L -> prime
    | number' -> nextPrime(prime) |> findNextPrimeFactor number'

Аргументов больше нет:)


Хорошо, теперь, когда это у нас в стороне, давайте посмотрим на вашу isPrime функцию:

let isPrime(n:int64) = 
    let rec check(i:int64) = 
        i > n / 2L or (n % i <> 0L && check(i + 1L))
    check(2L) 

Вы, наверное, слышали, чтобы использовать рекурсию вместо циклов, и это правильно. Но, где это возможно, вы должны абстрагироваться от рекурсии с помощью сгибов, карт или функций более высокого порядка. Для этого есть две причины:

  1. немного более читабельно, а

  2. неправильно записанная рекурсия приведет к переполнению стека. Например, ваша функция не является хвостовой рекурсией, поэтому она будет взорвана при больших значениях n.

Я бы переписал isPrime так:

let isPrime n = seq { 2L .. n / 2L } |> Seq.exists (fun i -> n % i = 0L) |> not

В большинстве случаев, если вы можете абстрагироваться от явного зацикливания, тогда вы просто применяете преобразования к входной последовательности, пока не получите результаты:

let maxFactor n =
    seq { 2L .. n - 1L }                        // test inputs
    |> Seq.filter isPrime                       // primes
    |> Seq.filter (fun x -> n % x = 0L)         // factors
    |> Seq.max                                  // result

У нас даже нет промежуточных переменных в этой версии. Прохлада!


Мой второй приоритет - мне бы хотелось Быть быстрым и эффективным.

В большинстве случаев F # будет довольно сравнимым с C # с точки зрения скорости или "достаточно быстрым". Если вы обнаружите, что выполнение вашего кода занимает много времени, это, вероятно, означает, что вы используете неправильную структуру данных или неверный алгоритм. Для конкретного примера прочитайте комментарии к этому вопросу .

Итак, код, который я написал, «элегантен» в том смысле, что он лаконичен, дает правильные результаты и не использует никаких хитростей. К сожалению, это не очень быстро. Для начала:

  • он использует пробное деление для создания последовательности простых чисел, когда Сито Эратосфена будет намного быстрее. [Редактировать: я написал несколько наивную версию этого сита, которая не работала для чисел, больших, чем Int32.MaxValue, поэтому я удалил код.]

  • прочитайте статью в Википедии о функции подсчета простых чисел , она даст вам советы по вычислению первых n простых чисел, а также по оценке верхней и нижней границ для простого числа nth .

[Edit: я включил некоторый код с несколько наивной реализацией сита из эратосфена. Он работает только для входных данных, меньших int32.MaxValue, поэтому, вероятно, он не подходит для проекта euler.]

5 голосов
/ 10 января 2010

Относительно «хорошей функциональной привычки» или, скорее, хорошей практики, я вижу три незначительные вещи. Использовать yield в вашей последовательности немного сложнее, чем просто фильтровать. Ненужные аннотации типов в предполагаемом языке типов затрудняют рефакторинг и затрудняют чтение кода. Не переусердствуйте и попробуйте удалить все аннотации типов, если вам это трудно. Наконец, создание лямбда-функции, которая принимает только значение для использования в качестве временной переменной, снижает читабельность.

Что касается личного стиля, я предпочитаю больше пробелов и использую аргументы с кортежем только тогда, когда данные имеют смысл объединять вместе.

Я бы написал ваш оригинальный код следующим образом.

let isPrime n = 
    let rec check i = 
        i > n / 2L || (n % i <> 0L && check (i + 1L))
    check 2L

let greatestPrimeFactor n =
    let nextPrime prime = 
        seq {prime + 1L .. System.Int64.MaxValue}
        |> Seq.filter isPrime
        |> Seq.skipWhile (fun v -> n % v <> 0L) 
        |> Seq.head

    let rec findNextPrimeFactor number prime =
        if number = 1L then 
            prime 
        else 
            let p = nextPrime(prime)
            findNextPrimeFactor (number / p) p

    findNextPrimeFactor n 2L

Ваш обновленный код оптимален для вашего подхода. Вам придется использовать другой алгоритм, такой как ответ Инь Чжу, чтобы идти быстрее. Я написал тест, чтобы проверить, делает ли F # рекурсивную функцию "check", и это делает.

3 голосов
/ 10 января 2010

переменная p на самом деле является привязкой имени, а не переменной. Использование привязки имени не является плохим стилем. И это более читабельно. Ленивый стиль nextPrime хорош, и он фактически тестирует каждое число только один раз в течение всей программы.

Мое решение

let problem3 = 
    let num = 600851475143L
    let rec findMax (n:int64) (i:int64) =
        if n=i || n<i then
            n
        elif n%i=0L then
            findMax (n/i) i
        else
            findMax n (i+1L)
    findMax num 2L

Я делю num на 2, 3, 4 ... и не учитываю простых чисел. Потому что если мы разделим все 2 от num, то мы не сможем разделить его на 4,8 и т. Д.

по этому номеру мое решение быстрее:

> greatestPrimeFactor 600851475143L;;
Real: 00:00:01.110, CPU: 00:00:00.702, GC gen0: 1, gen1: 1, gen2: 0
val it : int64 = 6857L
> 
Real: 00:00:00.001, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0

val problem3 : int64 = 6857L
1 голос
/ 10 января 2010

Я думаю, что код с временной привязкой значительно легче читать. Довольно необычно создать анонимную функцию, а затем сразу применить ее к значению, как в другом случае. Если вы действительно хотите избежать использования временного значения, я думаю, что наиболее идиоматичным способом сделать это в F # будет использование оператора (|>) для передачи значения в анонимную функцию, но я все же думаю, что это вполне читабельно.

...