Схема, стратегии оптимизации N-ферзей, глава 2 SICP

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

Эта стратегия взрывается примерно после n = 11 с максимальной ошибкой рекурсии.

Я реализовал альтернативную стратегию, которая делает более разумный обход дерева из первого столбца, генерируя возможные позиции из списка неиспользуемых строк, объединяя каждый список позиций в обновленный список еще неиспользованных строк. Фильтрация тех пар, которые считаются безопасными, и рекурсивное сопоставление этих пар для следующего столбца. Это не взорвалось (пока), но n = 12 занимает минуту, а n = 13 занимает около 10 минут.

(define (queens board-size)
 (let loop ((k 1) (pp-pair (cons '() (enumerate-interval 1 board-size))))
   (let ((position (car pp-pair))
         (potential-rows (cdr pp-pair)))
    (if (> k board-size) 
        (list position)
        (flatmap (lambda (pp-pair) (loop (++ k) pp-pair)) 
         (filter (lambda (pp-pair) (safe? k (car pp-pair))) ;keep only safe
          (map (lambda (new-row) 
                (cons (adjoin-position new-row k position) 
                      (remove-row new-row potential-rows))) ;make pp-pair
           potential-rows))))))) 
;auxiliary functions not listed 

На самом деле не ищу кода, а просто объясняю пару стратегий, которые менее наивны и хорошо подходят для функционального подхода.


person WorBlux    schedule 09.06.2013    source источник
comment
Что вы имеете в виду под «взрывами»? Если реализация Scheme терпит неудачу, какую реализацию вы используете?   -  person GoZoner    schedule 09.06.2013
comment
Здесь: stackoverflow.com/q/2595132/1286639   -  person GoZoner    schedule 09.06.2013
comment
@ GoZoner, максимальная ошибка рекурсии возникает при n 11 или больше, но не при n 10 или меньше. Используя Mit-схему,   -  person WorBlux    schedule 10.06.2013
comment
Вы можете увеличить размер стека для мит-схемы; это позволит избежать ошибки рекурсии, которую вы видите. Попробуйте mit-scheme --stack <number-of-1024-blocks>. Я знаю, это не отвечает на ваш вопрос об алгоритме.   -  person GoZoner    schedule 10.06.2013
comment
В видеолекциях Хэл Абельсон использовал эту задачу для демонстрации потоков.   -  person Sylwester    schedule 10.06.2013


Ответы (2)


Я могу предложить вам упрощение вашего кода, чтобы он работал немного быстрее. Начнем с переименования некоторых переменных для улучшения читаемости (YMMV),

(define (queens board-size)
 (let loop ((k 1) 
            (pd (cons '() (enumerate-interval 1 board-size))))
   (let ((position (car pd))
         (domain   (cdr pd)))
    (if (> k board-size) 
        (list position)
        (flatmap (lambda (pd) (loop (1+ k) pd)) 
         (filter (lambda (pd) (safe? k (car pd))) ;keep only safe NewPositions
          (map (lambda (row) 
                (cons (adjoin-position row k position)  ;NewPosition
                      (remove-row row domain))) ;make new PD for each Row in D
               domain)))))))                            ; D

Теперь filter f (map g d) == flatmap (\x->let {y=g x} in [y | f y]) d (используя там немного синтаксиса Haskell), т.е. мы можем объединить map и filter в один flatmap:

        (flatmap (lambda (pd) (loop (1+ k) pd)) 
         (flatmap (lambda (row)                   ;keep only safe NewPositions
               (let ( (p (adjoin-position row k position))
                      (d (remove-row row domain)))
                 (if (safe? k p) 
                     (list (cons p d)) 
                     '())))
            domain)) 

затем flatmap h (flatmap g d) == flatmap (h <=< g) d (где <=< - оператор композиции Клейсли с направлением справа налево, но кого это волнует), чтобы мы могли объединить два flatmap в один, с

        (flatmap 
            (lambda (row)                         ;keep only safe NewPositions
                (let ((p (adjoin-position row k position)))
                  (if (safe? k p)
                    (loop (1+ k) (cons p (remove-row row domain)))
                    '())))
            domain)

поэтому упрощенный код

(define (queens board-size)
 (let loop ((k        1) 
            (position '())
            (domain   (enumerate-interval 1 board-size)))
    (if (> k board-size) 
        (list position)
        (flatmap 
            (lambda (row)                         ;use only the safe picks
              (if (safe_row? row k position)      ;better to test before consing
                (loop (1+ k) (adjoin-position row k position)
                             (remove-row row domain))
                '()))
            domain))))
person Will Ness    schedule 12.06.2013
comment
В этом есть большой смысл комбинировать операции, которые рекурсивно проходят вниз по cdr списка вместе. Спасибо. Я также подумываю заменить domain списком доменов, начинающихся с длинной k, и remove-row функцией, которая отмечает строки и диагонали в остальной части доски, устраняя необходимость в безопасном тесте, - person WorBlux; 13.06.2013
comment
@WorBlux, вы можете попробовать представить домены в виде сбалансированных деревьев, а не списков, для уменьшения времени обновления. Используя векторы для представления доменов, обновление происходит быстрее всего, но вам придется копировать векторы, а копировать можно за O (n); с деревьями вы делаете обновленную копию за то же время O (log n), что и мутация. Но n очень маленький (8, 10, 13), поэтому стоит проверить оба. Также неясно, стоит ли поддерживать диагонали или делать несколько простых математических операций. Братко сделал это, правда, на Прологе. - person Will Ness; 13.06.2013
comment
@WorBlux о деревьях, цель - ускориться remove-row. Возможно, вы могли бы сэкономить несколько циклов, объединив это с выбором возможностей из домена, ценой предварительного вычисления всей этой структуры: в Haskell это pick . Конечно, в схеме он должен быть сгенерирован эффективно, в сверху вниз, TRMC мода. Зная заранее длину списка доменов, может помочь. - person Will Ness; 13.06.2013
comment
упс, пропустил тег mit-scheme, re: the edit. действительно там работал код AFAICR. тем не менее, лучше, чтобы код был совместимым. - person Will Ness; 08.06.2021

Вот что я придумал во второй раз. Хотя не уверен, что это намного быстрее. Хотя немного красивее.

(define (n-queens n)
  (let loop ((k 1) (r 1) (dangers (starting-dangers n)) (res '()) (solutions '()))
    (cond ((> k n) (cons res solutions))
          ((> r n) solutions)
          ((safe? r k dangers) 
           (let ((this (loop (+ k 1) 1 (update-dangers r k dangers) 
                             (cons (cons r k) res) solutions)))
             (loop k (+ r 1) dangers res this)))
          (else (loop k (+ r 1) dangers res solutions)))))

Важным моментом является использование оператора let для сериализации рекурсии с ограничением глубины до n. Решения приходят в обратном направлении (возможно, можно исправить, перейдя n-> 1 вместо 1-> n для r и k), но набор в обратном направлении - это тот же набор, что и набор frowards.

(define (starting-dangers n)
  (list (list)
        (list (- n))
        (list (+ (* 2 n) 1))))
;;instead of terminating in null list, terminate in term that cant threaten

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

(define (safe? r k dangers)
   (and (let loop ((rdangers (rdang dangers)))
           (cond ((null? rdangers) #t)
                 ((= r (car rdangers))
                  #f)
                 (else (loop (cdr rdangers)))))
        (let ((ddiag (- k r)))
           (let loop ((ddangers (ddang dangers)))
              (if (<= (car ddangers) ddiag)
                  (if (= (car ddangers) ddiag)
                      #f
                      #t)
                  (loop (cdr ddangers)))))
        (let ((udiag (+ k r)))
           (let loop ((udangers (udang dangers)))
              (if (>= (car udangers) udiag)
                  (if (= (car udangers) udiag)
                      #f
                      #t)
                  (loop (cdr udangers)))))))

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

(define (update-dangers r k dangers)
  (list
     (cons r (rdang dangers))
     (insert (- k r) (ddang dangers) >)
     (insert (+ k r) (udang dangers) <))) 

 (define (insert x sL pred)
   (let loop ((L sL))
      (cond ((null? L) (list x))
            ((pred x (car L))
             (cons x L))
            (else (cons (car L)
                        (loop (cdr L)))))))

(define (rdang dangers)
  (car dangers))
(define (ddang dangers)
  (cadr dangers))
(define (udang dangers)
  (caddr dangers))
person WorBlux    schedule 10.12.2015