Код Lisp не отвечает

Спойлер: это ответ на вопрос номер 7 в Project Euler.

Я изучаю Lisp и использовал compileonline.com для запуска своего кода. Однако в простой программе не хватило памяти, поэтому я переключился на настольную версию. Однако даже на это не хватает памяти. Может кто-нибудь сказать мне, почему это так плохо?

В текущем виде он не зависает, но если я изменю его на:

(if (= primecount 10001) 

он висит. Даже что-то такое маленькое, как 1000 зависает.

(setq current 3)
(setq primecount 1)
(setq primes (list 2))
(setq isprime "yes")

(loop for x from current do
  (list 
    (setq isprime "yes")
    (loop 
      for prime in primes do 
        (if (= (mod current prime) 0) 
          (list (setq isprime nil) (return)) 
          nil
        )
    )
    (if isprime 
      (list 
        (setq primecount (+ primecount 1)) 
        (setq primes (cons current primes)) 
        (if (= primecount 100) 
          (return) 
          nil
        )
      )
      (setq current (+ current 1))
    )
  )
)

(write-line (write-to-string primes))

person Millie Smith    schedule 25.12.2013    source источник


Ответы (3)


TL;DR: проблема не в памяти, а в скорости.

Вы были очень изобретательны в преодолении недостатка знаний. :) На самом деле, у нас есть progn в Common Lisp, который группирует несколько вычисляемых выражений по порядку:

(defun myprimes-to (n &aux          ;; &aux vars may have init values
                      (current 3)
                      (primecount 1)
                      (isprime t) ; "yes"   ;; t is better 
                      (primes (list 2))
    (loop  ;for x from current do   ;; for a bare loop, just `loop` is enough
      (progn  ;list                 ;; `progn` groups expressions together to be
        (setq isprime t) ; "yes"    ;;    evaluated in order, one after another
        (loop                       ;;      (not even needed here, see comments below)
          for prime in primes do 
            (if (= (mod current prime) 0) 
                (progn  ;list
                  (setq isprime nil) 
                  (return))         ;; exit the loop
                nil))               ;; don't have to have alternative clause
        (if isprime 
            (progn  ;list 
              (incf primecount)  ; (setq primecount (+ primecount 1)) ;; same but shorter
              (setq primes (cons current primes)) 
              (if (= primecount n)  ;; abstract the n as function's argument
                  (return)          ;; exit the loop
                  nil))
            ;; What?? increment your loop var _only_ if it's not prime?
            (setq current (+ current 1)))))
    ;; (write-line (write-to-string primes))
    primes)                         ;; just return them

Поэтому, если current является простым, попытка повторяется. Поскольку в последний раз он был добавлен в primes, теперь он будет считать себя не простым, и итерация продолжится. Результаты правильные, но логика искажена.

Что еще более важно, здесь есть две основные неэффективности с точки зрения алгоритма: вы тестируете все primes до сих пор и обратно. Но у любого заданного числа гораздо больше шансов иметь меньший делитель, чем больший; поэтому мы должны проверять по простым числам в возрастающем порядке. И мы можем остановиться, когда простое число p равно p*p > current, потому что если current == a*b и a <= b, то a*a <= b*a == current. Ранняя остановка дает огромное ускорение, она изменяет сложность времени выполнения с ~ n^2 до, грубо говоря, ~ n^1,5 (в произведенных n простых числах).


Обратите внимание: ваш код хуже, чем ~ n^2, алгоритмически. Было бы ~ n^2, если бы проверялись простые числа в порядке возрастания. -- Почему это важно? Это дает частичный ответ на ваш вопрос: у вас проблемы не с памятью, а с скоростью. Сколько бы времени ни потребовалось вашему коду для завершения n=100, для перехода к n=1000 потребуется более чем в 100 раз (поскольку 1000/100 == 10, а 10^2 = 100). И чтобы добраться до n=10000, потребуется в 100 раз больше времени. Таким образом, если задача n=100 заняла секунду времени выполнения, n=1000 займет 100 секунд (~ 2 минуты), а n=10001 200 минут (более чем через 3 часа).

Мы всегда можем получить оценки порядков роста во время выполнения эмпирически; см. эту статью о WP.


Таким образом, вам нужно поддерживать primes в порядке возрастания и append каждого нового простого числа до конца за время O(1). Вам нужно добиться тех же результатов, что и строка кода (setq primes (append primes (list current))); но вы не можете использовать эту строку кода, потому что это O (n) по времени и резко замедлит работу всей программы. То же самое с (setq primes (reverse (cons current (reverse primes)))). И мы не можем поддерживать primes в обратном порядке, потому что мы должны тестировать в порядке возрастания, и простой вызов (reverse primes) для каждого нового current также является операцией времени O(n).

Решение состоит в том, чтобы использовать rplacd: (rplacd (last primes) (list current)). Даже это все равно неправильно, потому что last тоже операция O(n). Вместо этого вам придется поддерживать дополнительную переменную, указывающую на последнюю cons-ячейку списка простых чисел, и продвигать ее каждый раз, когда вы добавляете туда новое простое число: (setq last_cell (cdr last_cell)).

Когда вы запустите фиксированный код, вы обнаружите, что он завершает задание 10 000 примерно за 0,1 секунды и работает примерно за ~ n^1,4 порядки роста во время выполнения в этом диапазоне.

person Will Ness    schedule 26.12.2013
comment
но так как это loop, мы можем даже просто сделать loop … when … do … для внутреннего loop, а для внешнего цикла нет необходимости в (loop (progn …)); мы можем просто сделать (loop …). - person Joshua Taylor; 26.12.2013
comment
@JoshuaTaylor или prog вместо цикла с ручным gotos. ;) :) Просто хотел внести туда минимальные изменения. - person Will Ness; 26.12.2013
comment
@JoshuaTaylor (мне нравится prog, это сентиментальная слабость). :) Но кроме того, голый loop очень просто настроить - здесь, например. нам нужно выйти пораньше, (if (> (* prime prime) current) (return)). Легко просто добавить его и не думать о пунктах loop и их взаимодействии... Меня на самом деле вдохновляет бесстрашная простота OP-кода. :) - person Will Ness; 26.12.2013
comment
@WillNess, лол, хорошая повторная попытка каждого простого числа. Спасибо за подробный ответ. Все еще пытаюсь полностью понять ваш анализ времени выполнения, хотя в основном это имеет смысл. На самом деле я пытался избежать вставки O (n) и допустил еще большую ошибку. Я нахожу интересным, что никто не исправил мой слишком широкий охват isprime. И никто не использовал let ... Кроме того, нет ли лучшей структуры данных в lisp, которая дает вставку O (1). - person Millie Smith; 27.12.2013
comment
Хахаха бесстрашная простота :) - person Millie Smith; 27.12.2013
comment
Я все еще думаю, что код n ^ 2. Если я ищу n-е простое число, ни одна проверка на простоту не требует более n операций. Вставка в моем коде - O (1), поэтому код сводится к суммированию от 1 до n, что составляет O (n ^ 2) - person Millie Smith; 27.12.2013
comment
пытаясь избежать вставки, 1-й фокус должен быть на алгоритме, структуры данных приходят 2-й - ваш внутренний цикл, for prime in primes, никогда не будет работать до конца; всегда будет обрезаться - на (not (zerop (mod current prime))) или (> (* prime prime) current) - это условие вы должны добавить в него, чтобы обрезать его рано и для простых чисел, а не только для составных. -- там, где работает &aux, нам не нужен let. -- относительно сложности, завершите функцию и измерьте ее . :) (тоже сделайте primes & aux var, как в ответе Райнера). - person Will Ness; 27.12.2013
comment
@MillieSmith Я протестировал его для n = 4000, n = 8000 и n = 10 000, и он работает так же быстро, как и у Райнера (в конце концов, различия только косметические), при ~ n ^ 1,4, как и ожидалось. Никогда не тестируйте его только в одной точке размера, 3 должно быть достаточно, чтобы увидеть, как он работает. -- не забудьте (compile 'myprimes) ! :)) -- Если вам трудно получить окончательный рабочий код, вы всегда можете опубликовать новый вопрос с вашим новым кодом на SO. :) -- также, (setq isprime t), не да, строки не нужны. - person Will Ness; 27.12.2013

Вы можете объяснить

  • что за переменная X и почему она не используется?

  • что делают звонки LIST?

  • зачем вы используете все эти глобальные переменные?

  • почему вы не определяете никаких функций?

  • почему код перебирает все простые числа?

Вот рефакторинг и немного улучшенная версия, основанная на вашем коде:

(defun find-primes (n &aux (primes (list 2)) (l-primes primes) (n-primes 1))
  "returns a list of the first n primes"
  (flet ((prime-p (i &aux (limit (isqrt i)))
           "returns T if i is a prime number"
           (loop for prime in primes
                 until (> prime limit)
                 when (zerop (mod i prime))
                 do (return-from prime-p nil))
           t))
    (loop for i from 3
          until (>= n-primes n)
          do (when (prime-p i)
               (setf (cdr l-primes) (list i)
                     l-primes (cdr l-primes))
               (incf n-primes))))
  primes)

(print (find-primes 10000))

Особенности вышеуказанной функции:

  • он хранит список простых чисел, увеличивая
  • он сохраняет ссылку на последние cons списка простых чисел, чтобы эффективно добавлять в конец списка
  • у него есть предикатная подфункция prime-p, которая возвращает T, если число является простым
  • предикат использует функцию isqrt для ограничения числа для поиска

С проверкой на N и локальным макросом для проталкивания в конец это выглядит так:

(defun find-primes (n &aux (primes (list 2)) (l-primes primes) (n-primes 1))
  "returns a list of the first n primes"
  (check-type n (integer 1))
  (flet ((prime-p (i &aux (limit (isqrt i)))
           "returns T if i is a prime number"
           (loop for prime in primes
                 until (> prime limit)
                 when (zerop (mod i prime))
                 do (return-from prime-p nil))
           t))
    (macrolet ((push-last (obj place)
                 `(setf (cdr ,place) (list ,obj)
                        ,place (cdr ,place))))
      (loop for i from 3
            until (>= n-primes n)
            do (when (prime-p i)
                 (push-last i l-primes)
                 (incf n-primes)))))
  primes)

Давай попробуем:

CL-USER 3 > (time (find-primes 10000))
Timing the evaluation of (FIND-PRIMES 10000)

User time    =        0.052
System time  =        0.000
Elapsed time =        0.044
Allocation   = 292192 bytes
person Rainer Joswig    schedule 25.12.2013
comment
Спасибо за ответ. 1. зацикливаться до условия 2. Позволить мне запускать несколько строк кода 3. Недостаток знаний 4. Мне хотелось упростить эту первую программу 5. Потому что задача Эйлера состоит в том, чтобы найти 10001-е простое число. - person Millie Smith; 26.12.2013
comment
@Millie Smith: 1) итерация по переменной от начала до конца. Вы этого не делаете. Вы делаете что-то совершенно странное. Вы вообще не используете объявленную переменную итерации и увеличиваете начало на каждой итерации. 2) PROGN 5) зачем тогда нужно тестировать все простые числа из списка? - person Rainer Joswig; 26.12.2013
comment
@rainierjoswig Тогда как мне зациклиться на 10001-м простом числе? 2. Спасибо, посмотрю. 5. Потому что составное число должно делиться на одно из простых чисел под ним. Предполагалось оптимизировать решение. - person Millie Smith; 26.12.2013
comment
@ Милли Смит: 1) вопрос не в этом. Вопрос в том, почему вы вводите X, но на самом деле повторяете current. 5) Верно, но вам не нужно смотреть на все это. - person Rainer Joswig; 26.12.2013
comment
Хм, я попробую этот код, как только получу интернет. Я заинтригован вашими результатами скорости. Спасибо! - person Millie Smith; 27.12.2013

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

Я немного переписал:

(defvar current)
(setf current 3)
(defvar primecount)
(setf primecount 1)
(defvar primes)
(setf primes (list 2))
(defvar isprime)

(loop for x from current do
  (setq isprime t)
  (loop 
    for prime in primes do 
      (when (zerop (mod current prime))
        (setq isprime nil) (return)))
  (if isprime 
      (progn 
        (incf primecount)
        (push current primes)
        (when (= primecount 10001) 
          (return)))
      (incf current)))
(write-line (write-to-string primes))

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

Ваша программа работает в моем SBCL для 10001 следующим образом (обернутая в функцию выполнения):

CL-USER> (time (do-stuff))
Evaluation took:
  13.486 seconds of real time
  13.438000 seconds of total run time (13.437000 user, 0.001000 system)
  99.64% CPU
  40,470,003,369 processor cycles
  1,674,208 bytes consed

CL-USER> (room)
Dynamic space usage is:   134,466,112 bytes.
Read-only space usage is:      5,856 bytes.
Static space usage is:         4,032 bytes.
Control stack usage is:        8,808 bytes.
Binding stack usage is:        1,056 bytes.
Control and binding stack usage is for the current thread only.
Garbage collection is currently enabled.

Breakdown for dynamic space:
  45,803,376 bytes for   569,191 instance objects.
  22,831,488 bytes for 1,426,968 cons objects.
  16,472,576 bytes for    16,135 code objects.
  15,746,176 bytes for   130,439 simple-vector objects.
  13,794,096 bytes for    36,381 simple-character-string objects.
  19,850,400 bytes for   440,297 other objects.
  134,498,112 bytes for 2,619,411 dynamic objects (space total.)
person sid_cypher    schedule 26.12.2013
comment
на самом деле, создается только один список простых чисел путем добавления сверху, поэтому 1000 или 10 000 простых чисел занимают очень мало места. - person Will Ness; 26.12.2013
comment
нет причин для loop … do (when test form…); loop … when test do form… в порядке. - person Joshua Taylor; 26.12.2013
comment
@JoshuaTaylor, ты совершенно прав; я просто хотел заменить исходную (if cond (progn...) nil) форму на тот момент. - person sid_cypher; 26.12.2013