N-королев в Хаскеле без обхода списка - PullRequest
17 голосов
/ 10 августа 2009

Я искал в Интернете различные решения проблемы n-queens в Haskell, но не смог найти ни одного, который мог бы проверять небезопасные позиции за время O (1), например тот, в котором у вас есть массив для / diagonals иодин для \ diagonals.

Большинство решений, которые я нашел, просто проверяли каждую новую королеву на соответствие всем предыдущим.Примерно так: http://www.reddit.com/r/programming/comments/62j4m/nqueens_in_haskell/

nqueens :: Int -> [[(Int,Int)]]
nqueens n = foldr qu [[]] [1..n]
    where qu k qss = [ ((j,k):qs) | qs <- qss, j <- [1..n], all (safe (j,k)) qs ]
      safe (j,k) (l,m) = j /= l && k /= m && abs (j-l) /= abs (k-m)

Какой наилучший способ реализовать такой подход «O (1)» в Haskell?Я не ищу ничего "сверхоптимизированного".Просто какой-нибудь способ получить "эта диагональ уже используется?"массив в функциональном порядке.

ОБНОВЛЕНИЕ:

Спасибо за все ответы, ребята!Причина, по которой я изначально задал этот вопрос, заключается в том, что я хотел решить более сложную проблему возврата.Я знал, как решить эту проблему на императивном языке, но не мог легко придумать чисто функциональную структуру данных для выполнения этой работы.Я подумал, что проблема с ферзями была бы хорошей моделью (являющейся проблемой обратного отслеживания :)) для общей проблемы структуры данных, но это не моя проблема real .1017 *

На самом деле я хочу найти структуру данных, которая допускает O (1) произвольный доступ и содержит значения, которые находятся либо в «начальном» состоянии (свободная линия / диагональ, в случае n-ферзей), либо в «»конечное состояние (занятая линия / диагональ), с переходами (свободными в занятые), равными O (1).Это может быть реализовано с использованием изменяемых массивов в императивном языке, но я чувствую, что ограничение обновления значений допускает только хорошую чисто функциональную структуру данных (в отличие от Quicksort, например, что действительно хочет изменяемые массивы).

Я полагаю, что решение sth так же хорошо, как вы можете получить, используя неизменяемые массивы в Haskell, и функция "main" выглядит так, как я хотел:

-- try all positions for a queen in row n-1
place :: BoardState -> Int -> [[(Int, Int)]]
place _ 0 = [[]]
place b n = concatMap place_ (freefields b (n-1))
   where place_ p = map (p:) (place (occupy b p) (n-1))

MainПохоже, проблема заключается в поиске лучшей структуры данных, так как массивы Haskell имеют обновление O (n).Другие хорошие предложения не соответствуют мифическому O (1) святому Граалю:

  • DiffArrays подходят близко, но путаются в обратном пути.Они на самом деле получают супер медленно :(.
  • STUArrays конфликтуют с довольно функциональным подходом возврата, поэтому они отбрасываются.
  • Карты и наборы имеют только O (log n) обновление.

Я не совсем уверен, что решение в целом существует, но оно кажется многообещающим.

ОБНОВЛЕНИЕ:

Самая многообещающая структура данных, которую я нашел там, где массивы трейлеров. В основном это DiffArray Haskell, но он мутирует обратно при возврате.

Ответы [ 5 ]

6 голосов
/ 10 августа 2009

Вероятно, наиболее простым способом было бы использовать UArray (Int, Int) Bool для записи безопасных / небезопасных битов. Хотя копирование это O (n 2 ), для небольших значений N это самый быстрый доступный метод.

Для больших значений N есть три основных варианта:

  • Data.DiffArray удаляет издержки копирования до тех пор, пока вы никогда не будете использовать старые значения снова после их изменения . То есть, если вы всегда выбрасываете старое значение массива после его изменения, модификация будет O (1). Однако если позднее вы получите доступ к старому значению массива (даже только для чтения), O (N 2 ) будет выплачено полностью.
  • Data.Map и Data.Set разрешают O (lg n) модификаций и поисков. Это меняет алгоритмическую сложность, но часто достаточно быстро.
  • Data.Array.ST STUArray s (Int, Int) Bool предоставит вам обязательные массивы, которые позволят вам реализовать алгоритм классическим (нефункциональным) способом.
5 голосов
/ 12 августа 2009

В общем, вы, вероятно, застрянете, платя налог за сложность O(log n) за функциональную неразрушающую реализацию, или вам придется смягчиться и использовать (IO|ST|STM)UArray.

Строгие чистые языки, возможно, должны будут заплатить налог O(log n) за нечистый язык, который может писать в ссылки путем реализации ссылок через структуру, подобную карте; Ленивые языки могут иногда уклоняться от этого налога, хотя нет никаких доказательств того, достаточна ли дополнительная мощность, предлагаемая ленью, чтобы всегда уклоняться от этого налога - даже если есть серьезные основания полагать, что лень недостаточно сильна.

В этом случае трудно увидеть механизм, с помощью которого можно использовать лень, чтобы избежать ссылочного налога. И, в конце концов, именно поэтому мы имеем монаду ST. ;)

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

3 голосов
/ 25 августа 2011

Я скептически отношусь к утверждению , что чистый функционал - это, как правило, O (log n). См. Также ответ Эдварда Кметта, который делает это утверждение. Хотя это может применяться к случайному доступу к изменяемым массивам в теоретическом смысле, но доступ к случайным изменяемым массивам, вероятно, не тот, который требуется большинству любого алгоритма, когда он должным образом изучен для повторяемой структуры, то есть не является случайным. Я думаю, что Эдвард Кметт ссылается на это, когда пишет «эксплуатировать локальность обновлений».

Я думаю, что O (1) теоретически возможно в чисто функциональной версии алгоритма n-queens, добавив метод отмены для DiffArray, который запрашивает просмотр различий, чтобы удалить дубликаты и избежать их воспроизведения.

Если я правильно понимаю, как работает алгоритм возврата n-ферзей, то замедление, вызванное DiffArray, связано с сохранением ненужных различий.

В реферате «DiffArray» (не обязательно Haskell) имеет (или может иметь) метод элемента set, который возвращает новую копию массива и сохраняет разностную запись с оригинальной копией, включая указатель на новую измененная копия. Когда исходная копия должна получить доступ к элементу, этот список различий необходимо воспроизвести в обратном порядке, чтобы отменить изменения в копии текущей копии. Обратите внимание, что есть даже издержки, что этот односвязный список нужно пройти до конца, прежде чем его можно будет воспроизвести.

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

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

Работая с этим только в моей голове, я представляю, что причина, по которой DiffArray такой медленный, заключается в том, что когда ферзь перемещается из одной позиции в другую, тогда логический флаг для исходной позиции устанавливается обратно в ложь, а новый position устанавливается в true, и эти различия записываются, но они не нужны, потому что при обратном воспроизведении массив заканчивается теми же значениями, которые он имел до начала воспроизведения. Таким образом, вместо использования операции set для возврата к значению false, необходим вызов метода отмены, необязательно с входным параметром, сообщающим DiffArray, какое значение «отменять до» следует искать в вышеупомянутом двусвязном списке различий. Если это значение «отменить к» найдено в записи различий в двойном списке, нет конфликтующих промежуточных изменений в том же элементе массива, найденном при переходе назад в поиске по списку, и текущее значение равно «отменить из» значение в этой записи разницы, то запись может быть удалена, и эта старая копия может быть перенаправлена ​​на следующую запись в двойном списке.

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

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


ОБНОВЛЕНИЕ: я не написал код для всего алгоритма, но в моей голове n-королевы могут быть реализованы с каждой повторяющейся строкой, сгиб на следующем массиве диагоналей, где каждый элемент - триплетный кортеж из: (индекс строки занят или None, массив индексов строк, пересекающих диагональ влево-вправо, массив индексов строк, пересекающих диагональ вправо-влево). Строки можно повторять с помощью рекурсии или сгиба массива индексов строк (сгиб выполняет рекурсию).

Здесь приведены интерфейсы для структуры данных, которую я представляю. Синтаксис ниже - Copute, но я думаю, что он достаточно близок к Scala, чтобы вы могли понять, для чего он предназначен.

Обратите внимание, что любая реализация DiffArray будет неоправданно медленной, если она многопоточная, но алгоритм возврата n-queens не требует, чтобы DiffArray был многопоточным. Спасибо Эдварду Кметту за то, что он указал на это в комментариях к этому ответу.

interface Array[T]
{
   setElement  : Int -> T -> Array[T]     // Return copy with changed element.
   setElement  : Int -> Maybe[T] -> Array[T]
   array       : () -> Maybe[DiffArray[T]]// Return copy with the DiffArray interface, or None if first called setElement() before array().
}
// An immutable array, typically constructed with Array().
//
// If first called setElement() before array(), setElement doesn't store differences,
// array will return None, and thus setElement is as fast as a mutable imperative array.
//
// Else setElement stores differences, thus setElement is O(1) but with a constant extra overhead.
// And if setElement has been called, getElement incurs an up to O(n) sequential time complexity,
// because a copy must be made and the differences must be applied to the copy.
// The algorithm is described here:
//    /1329309/n-korolev-v-haskele-bez-obhoda-spiska
// Similar to Haskell's implementation:
//    http://www.haskell.org/haskellwiki/Arrays#DiffArray_.28module_Data.Array.Diff.29
//    http://www.haskell.org/pipermail/glasgow-haskell-users/2003-November/005939.html
//
// If a multithreaded implementation is used, it can be extremely slow,
// because there is a race condition on every method, which requires internal critical sections.

interface DiffArray[T] inherits Array[T]
{
   unset       : () -> Array[T]        // Return copy with the previous setElement() undone, and its difference removed.
   getElement  : Int -> Maybe[T]       // Return the the element, or None if element is not set.
}
// An immutable array, typically constructed with Array( ... ) or Array().array.

ОБНОВЛЕНИЕ: я работаю над реализацией Scala , которая имеет улучшенный интерфейс по сравнению с тем, что я предлагал выше. Я также объяснил, как оптимизация для складок приближается к тем же постоянным издержкам, что и изменяемый массив.

3 голосов
/ 10 августа 2009

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

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

import Data.Array.IArray (array, (//), (!))
import Data.Array.Unboxed (UArray)
import Data.Set (Set, fromList, toList, delete)

-- contains sets of unoccupied columns and lookup arrays for both diagonals
data BoardState = BoardState (Set Int) (UArray Int Bool) (UArray Int Bool)

-- an empty board
board :: Int -> BoardState
board n
   = BoardState (fromList [0..n-1]) (truearr 0 (2*(n-1))) (truearr (1-n) (n-1))
   where truearr a b = array (a,b) [(i,True) | i <- [a..b]]

-- modify board state if queen gets placed
occupy :: BoardState -> (Int, Int) -> BoardState
occupy (BoardState c s d) (a,b)
   = BoardState (delete b c) (tofalse s (a+b)) (tofalse d (a-b))
   where tofalse arr i = arr // [(i, False)]

-- get free fields in a row
freefields :: BoardState -> Int -> [(Int, Int)]
freefields (BoardState c s d) a = filter freediag candidates
   where candidates = [(a,b) | b <- toList c]
         freediag (a,b) = (s ! (a+b)) && (d ! (a-b))

-- try all positions for a queen in row n-1
place :: BoardState -> Int -> [[(Int, Int)]]
place _ 0 = [[]]
place b n = concatMap place_ (freefields b (n-1))
   where place_ p = map (p:) (place (occupy b p) (n-1))

-- all possibilities to place n queens on a n*n board
queens :: Int -> [[(Int, Int)]]
queens n = place (board n) n

Это работает и для n = 14 примерно на 25% быстрее, чем версия, которую вы упомянули. Основное ускорение происходит за счет использования распакованных массивов bdonian , рекомендуемых. При нормальном Data.Array время выполнения примерно такое же, как у версии, о которой идет речь.

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

1 голос
/ 27 августа 2011

У меня есть решение. Тем не менее, константа может быть большой, поэтому я не очень надеюсь победить.

Вот моя структура данных:

-- | Zipper over a list of integers
type Zipper = (Bool,  -- does the zipper point to an item?
               [Int], -- previous items
                      -- (positive numbers representing
                      --   negative offsets relative to the previous list item)
               [Int]  -- next items (positive relative offsets)
               )

type State =
  (Zipper, -- Free columns zipper
   Zipper, -- Free diagonal1 zipper
   Zipper  -- Free diagonal2 zipper
   )

Позволяет выполнять все необходимые операции в O (1).

Код можно найти здесь: http://hpaste.org/50707

Скорость плохая - она ​​медленнее, чем эталонное решение, опубликованное в вопросе на большинстве входов. Я сравнил их друг с другом на входах [1,3 .. 15] и получил следующие временные соотношения ((контрольное время решения / время моего решения) в%):

[24,66%, 19,89%, 23,74%, 41,22%, 42,54%, 66,19%, 84,13%, 106,30%]

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

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

В любом случае, я думаю, что в этой задаче O (1) и O (log (n)) в любом случае неразличимы, потому что log (8) - это всего лишь 3, и подобные константы являются предметом микрооптимизации, а не алгоритма.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...