Двумерная молния
Вдохновленный недавним вопросом о двумерных сетках в Haskell, я задаюсь вопросом, можно ли было бы создать двумерную молнию, чтобы отслеживать положение в списке списков. Одномерная застежка-молния в списке позволяет нам действительно эффективно перемещаться локально в большом списке (распространенным примером является текстовый редактор). Но допустим, у нас есть второе измерение, подобное этому:
grid =
[[ 1, 2, 3, 4, 5]
,[ 6, 7, 8, 9,10]
,[11,12,13,14,15]
,[16,17,18,19,20]
,[21,22,23,24,25]]
Можем ли мы создать какую-то структуру данных с застежкой-молнией для эффективного перемещения не только влево и вправо, но и вверх и вниз в сетке? Если так, то что, если мы заменим список списков бесконечным списком бесконечных списков, сможем ли мы получить эффективное движение?
2 ответа
Не совсем, нет. Одним из ключевых аспектов работы застежек-молний является то, что они представляют местоположение в структуре путем, используемым для его достижения, плюс дополнительные фрагменты, созданные по пути, с конечным результатом, по которому можно вернуться по этому пути и перестроить структуру как ты иди. Таким образом, характер путей, доступных через структуру данных, ограничивает застежку-молнию.
Поскольку местоположения идентифицируются путями, каждый отдельный путь представляет отдельное местоположение, поэтому любая структура данных с несколькими путями к одному и тому же значению не может использоваться с застежкой-молнией - например, рассмотрим циклический список или любую другую структуру с зацикливанием пути.
Произвольное движение в 2D-пространстве не совсем соответствует вышеуказанным требованиям, поэтому мы можем сделать вывод, что 2D-молния обязательно будет несколько ограничена. Возможно, вы начнете с начала координат, пройдете путь через структуру, а затем вернетесь назад по этому пути на некоторое расстояние, например, чтобы достичь других точек. Это также подразумевает, что для любой точки в структуре существуют другие точки, которые могут быть достигнуты только через начало координат.
Что вы можете сделать, так это встроить некоторое представление о двухмерном расстоянии в структуру данных, чтобы, следуя пути вниз по структуре, точки "ниже" находились рядом друг с другом; идея состоит в том, чтобы минимизировать количество возвратов, необходимых в среднем для перемещения на небольшое расстояние в 2D-пространстве. В конечном итоге это примерно такой же подход, который необходим для поиска 2D-пространства по расстоянию - поиск ближайшего соседа, эффективное геометрическое пересечение и тому подобное, - и его можно выполнить с такой же структурой данных, а именно с разделением пространства для создания более высокого уровня. дерево поиска. Реализация молнии для дерева квадрантов, дерева kd или подобных структур проста, как и любое другое дерево.
Ну, вы можете использовать что-то простое, как следующий код. Мы представляем таблицу верхними строками выбранного элемента, нижними строками выбранного элемента, плюс элементами слева от выбранного и элементами справа от выбранного.
Верхние ряды и левые элементы хранятся в обратном порядке, чтобы обеспечить эффективное перемещение.
Хотя я не уверен, что это можно считать молнией, потому что, хотя у нас есть "местоположение" в структуре данных, это не "путь".
-- Table sel left right top bottom
data Table a = Table a [a] [a] [[a]] [[a]] deriving Show
left :: Table a -> Table a
left tab@(Table _ [] _ _ _) = tab
left (Table sel (l:ls) rs ts bs) = Table l ls (sel:rs) ts bs
right :: Table a -> Table a
right tab@(Table _ _ [] _ _) = tab
right (Table sel ls (r:rs) ts bs) = Table r (sel:ls) rs ts bs
up :: Table a -> Table a
up tab@(Table _ _ _ [] _) = tab
up (Table sel ls rs (t:ts) bs) = Table sel' ls' rs' ts (b:bs)
where
(ls',(sel':rs')) = splitAt (length ls) t
b = ls ++ (sel:rs)
down :: Table a -> Table a
down tab@(Table _ _ _ _ []) = tab
down (Table sel ls rs ts (b:bs)) = Table sel' ls' rs' (t:ts) bs
where
(ls',(sel':rs')) = splitAt (length ls) b
t = ls ++ (sel:rs)
tableToList :: Table a -> [[a]]
tableToList (Table sel ls rs ts bs) = (reverse ts) ++ [ls ++ (sel:rs)] ++ bs
listToTable :: [[a]] -> Table a
listToTable [] = error "cannot make empty table"
listToTable ([]:_) = error "cannot make empty table"
listToTable ((t:tr):ts) = Table t [] tr [] ts
Это даже работает для бесконечных списков -
selected :: Table a -> a
selected (Table sel _ _ _ _) = sel
a :: Table Int
a = listToTable $ replicate 10 [1..]
selected a #=> 1
selected $ down a #=> 1
selected $ right $ down a #=> 2
Я искал что-то похожее: способ дешево и легко ориентироваться (который включает в себя переход "назад") дважды бесконечный список списков. Вот мой взгляд на это.
Если я внимательно прочитаю ответы других, то, что я здесь представляю, на самом деле не является застежкой-молнией: хотя навигация амортизируется O(1), память, используемая сетью структуры молнии, никогда не освобождается. С другой стороны, он должен быть достаточно узким для того, чтобы "ячейки" могли быть общими, независимо от того, какой путь мы выберем, - это такая топология, которую мы хотели бы видеть в двухмерном списке списков.
Чтобы компенсировать это, список списков, использованных для его генерации, в конечном итоге должен остаться без ссылок и для сбора мусора.
data FakeZip2D a = Z2 { z2Val :: a
, goUp :: !( Maybe (FakeZip2D a) )
, goDown :: Maybe (FakeZip2D a)
, goLeft :: !( Maybe (FakeZip2D a) )
, goRight :: Maybe (FakeZip2D a)
}
fromList2 :: [[a]] -> Maybe (FakeZip2D a)
fromList2 xss = head (head zss) where
extended = [ repeat Nothing ] ++
map (\z -> [Nothing] ++ z ++ repeat Nothing) zss ++
[ repeat Nothing ]
zss = zipWith4' row xss extended (drop 1 extended) (drop 2 extended)
row xs prev cur next = Just <$> zipWith5' Z2 xs (tail prev) (tail next)
cur (drop 2 cur)
-- totally inspired by https://stackru.com/a/54096748/12274
zipWith4' f (a:as) (b:bs) ~(c:cs) ~(d:ds) =
f a b c d : zipWith4' f as bs cs ds
zipWith5' f (a:as) (b:bs) ~(c:cs) (d:ds) ~(e:es) =
f a b c d e : zipWith5' f as bs cs ds es
Структура данных должна быть самоочевидной. Вверх и влево можно позволить себе быть строгим, потому что мы строим из односвязных списков. AFAIK, нет смысла делать их ленивыми в Хаскеле, так как они все равно не позволят им выйти за рамки возможного.
Решетка строится рекурсивно, расширяя границы предоставленного входа Nothing
, Достаточно ленивые варианты zipWith
Мне нужны были вдохновленные ответы на еще одну серию моих вопросов по теме.
Вот оно в действии:
demo :: IO ()
demo = do
let multList2 = [[ i*j | j <- [0..] ] | i <- [0..] ]
multZ2 = fromList2 multList2
let rows = iterate (>>= goDown) multZ2
cols = map (iterate (>>= goRight)) rows
putStrLn "Multiplication table"
mapM_ (print . map z2Val) $ take 5 $ map (catMaybes . take 5) cols
putStrLn "List of squares"
let goDiag = goRight >=> goDown
print $ map z2Val $ take 25 $ catMaybes $ iterate (>>= goDiag) multZ2
putStrLn "Convoluted list of squares"
let goDiag' = goDown >=> goRight >=> goUp >=> goLeft >=> goDiag
print $ map z2Val $ take 25 $ catMaybes $ iterate (>>= goDiag') multZ2
Интерфейс, вероятно, можно сделать еще проще, если Maybe
s. На свой страх и риск, естественно.
Это может быть немного не по теме, так как это не настоящая молния, но это решило мою проблему; и поскольку этот вопрос возник, когда я впервые искал решение, я публикую его здесь с намерением помочь кому-то другому.