N-королевы в Haskell без обхода списка

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

Большинство решений, которые я нашел, просто сравнивали каждую новую ферзя со всеми предыдущими. Примерно так: 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? Я не ищу ничего "сверхоптимизированного". Просто какой-то способ вывести "эта диагональ уже используется?" массив в функциональном виде.

ОБНОВЛЕНИЕ:

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

Я действительно хочу найти структуру данных, которая допускает произвольный доступ 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))

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

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

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

ОБНОВЛЕНИЕ:

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


person hugomg    schedule 10.08.2009    source источник
comment
Реализованы ли где-нибудь массивы трейлеров для Haskell?   -  person is7s    schedule 28.09.2012


Ответы (5)


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

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

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

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

person Edward KMETT    schedule 12.08.2009

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

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

  • Data.DiffArray удаляет накладные расходы на копирование , если вы больше никогда не используете старые значения после их изменения. То есть, если вы всегда выбрасываете старое значение массива после его изменения, модификация будет O (1). Если, однако, вы получите доступ к старому значению массива позже (даже только для чтения), O (N 2) оплачивается полностью.
  • Data.Map и Data.Set разрешить O (lg n ) модификации и поиски. Это меняет алгоритмическую сложность, но часто бывает достаточно быстро.
  • STUArray s (Int, Int) Bool из Data.Array.ST предоставит вам императивные массивы, позволяющие реализовать алгоритм классическим (нефункциональным) способом.
person bdonlan    schedule 10.08.2009
comment
Я думал, что обновления массива были O (n) и испортили сложность алгоритма. Я ошибся? - person hugomg; 10.08.2009
comment
Обновился - ты прав, но для малых N все равно дешевле :) - person bdonlan; 10.08.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 время выполнения примерно такое же, как и у версии, о которой идет речь.

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

person sth    schedule 10.08.2009

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

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

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

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

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

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

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

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

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


ОБНОВЛЕНИЕ: я не написал код для всего алгоритма, но в моей голове 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:
//    http://stackoverflow.com/questions/1255018/n-queens-in-haskell-without-list-traversal/7194832#7194832
// 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, которая имеет улучшенный интерфейс по сравнению с тем, что я предложил выше. Я также объяснил, как оптимизация складок приближается к тем же постоянным накладным расходам, что и изменяемый массив.

person Shelby Moore III    schedule 25.08.2011
comment
Прошло некоторое время с тех пор, как я в последний раз рассматривал эту проблему, но я не думаю, что это так просто ... - person hugomg; 25.08.2011
comment
Я работаю над реализацией и сообщу об этом позже. - person Shelby Moore III; 25.08.2011
comment
Алгоритм обратного отслеживания n-queen - это рекурсивная функция, которая принимает 3 параметра, массив для каждого направления диагоналей (\ и /) и количество строк. Он выполняет итерацию по столбцам в этой строке, перемещая позицию ферзя в этой строке в массивах и рекурсивно повторяя себя в каждой позиции столбца с cur_row + 1. Мне кажется, что перемещение ферзя в массивах невозможно, как я описал в моем ответе. Кажется, это слишком просто, не так ли. Кто-нибудь, пожалуйста, скажите мне, почему, или я узнаю, когда напишу реализацию в коде. - person Shelby Moore III; 25.08.2011
comment
Реализация DiffArray в Haskell имеет очень неудачные характеристики производительности, попытка поддерживать один фокус невидимо потенциально для нескольких потоков требует большого количества секретных встреч и синхронизации. Потери на 2-3 порядка из-за использования MVar под капотом перекрывают другие факторы алгоритма. Что касается «фактора журнала», я упомянул, что он применяется только в некоторых случаях, но в строго чистых языках он определенно применяется к некоторым алгоритмам, таким как поиск объединения. Я сознательно сказал, может, так как, возможно, можно было бы творчески амортизировать общее количество обновлений, чтобы отказаться от этого здесь. - person Edward KMETT; 26.08.2011
comment
@ShelbyMooreIII: Я думаю, что ваше описание использования двусвязных списков совпадает с моим первоначальным дизайном для моих коллекций Concestor, от которого я отказался, потому что его было слишком сложно реализовать правильно. Проблема в том, что дополнительные ссылки в списке обеспечивают постоянный доступ к истории, поэтому они должны быть слабыми ссылками, если вы хотите, чтобы ваши данные отмены / повтора собирались сборщиком мусора. В конце концов, я выбрал односвязные списки, образующие буферы отмены / повтора. Результат намного проще и по-прежнему полезен. На самом деле так просто, что это едва ли что-то новое. Просто очевидный эквивалент Set / Map DiffList - person J D; 25.12.2018
comment
@ShelbyMooreIII: Между прочим, другая точка зрения на это заключается в том, что алгоритмы, использующие чисто функциональные коллекции, которые не используют персистентность, могут быть написаны линейным образом и оптимизированы для использования императивных коллекций без (я думаю) жертвы чистоты). Может быть, Rust сможет это сделать? - person J D; 25.12.2018

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

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

-- | 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, а такие константы являются предметом микрооптимизации, а не алгоритма.

person Rotsor    schedule 27.08.2011