Координаты спирали, направленной наружу по часовой стрелке.

Я пытаюсь создать то, что, как мне кажется, называется спиралью Улама, с помощью Haskell. Он должен выходить наружу по часовой стрелке:

   6 - 7 - 8 - 9
   |           |
   5   0 - 1   10
   |       |   |
   4 - 3 - 2   11
               |
 ..15- 14- 13- 12

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

mkSpiral 9
> [(0,0),(1,0),(1,-1),(0,-1),(-1,-1),(-1,0),(-1,1),(0,1),(1,1)]
(-1, 1) - (0, 1) - (1, 1)
   |        
(-1, 0)   (0, 0) - (1, 0)
   |                 |
(-1,-1) - (0,-1) - (1,-1)

Я видел решение «Зацикливание по спирали», но оно идет против часовой стрелки, и его входные данные должны соответствовать размеру матрицы.

Я также нашел этот код, который делает то, что Мне нужно, но кажется, что он идет против часовой стрелки, скорее увеличивая, чем шагая вправо, а затем по часовой стрелке :(

type Spiral = Int
type Coordinate = (Int, Int)

-- number of squares on each side of the spiral
sideSquares :: Spiral -> Int
sideSquares sp = (sp * 2) - 1

-- the coordinates for all squares in the given spiral
coordinatesForSpiral :: Spiral -> [Coordinate]
coordinatesForSpiral 1 = [(0, 0)]
coordinatesForSpiral sp = [(0, 0)] ++ right ++ top ++ left ++ bottom
  where fixed = sp - 1
        sides = sideSquares sp - 1
        right = [(x, y) | x <- [fixed], y <- take sides [-1*(fixed-1)..]]
        top = [(x, y) | x <- reverse (take sides [-1*fixed..]), y <- [fixed]]
        left = [(x, y) | x <- [-1*fixed], y <- reverse(take sides [-1*fixed..])]
        bottom = [(x, y) | x <- take sides [-1*fixed+1..], y <- [-1*fixed]]

-- an endless list of coordinates (the complete spiral)
mkSpiral :: Int -> [Coordinate]
mkSpiral x = take x endlessSpiral

endlessSpiral :: [Coordinate]
endlessSpiral = endlessSpiral' 1

endlessSpiral' start = coordinatesForSpiral start ++ endlessSpiral' (start + 1)

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


person cmdv    schedule 20.08.2019    source источник
comment
Подсказка: что вам нужно, чтобы изменить спираль, чтобы повернуть ее против часовой стрелки к часовой?   -  person Willem Van Onsem    schedule 20.08.2019
comment
Сначала я бы попробовал записать на английском и / или математически, какой должна быть последовательность координат или как вы ее создадите. Как только у вас есть это, у вас есть что-то, что вы можете перевести в код.   -  person David Fletcher    schedule 20.08.2019
comment
он должен идти по часовой стрелке, мне только что дали отличное решение на канале резервирования FP, поэтому я могу опубликовать его здесь, если OP не получит времени :)   -  person cmdv    schedule 20.08.2019
comment
Боковое примечание: спираль Улама относится конкретно к спирали, выделяющей простые числа по часовой стрелке или против часовой стрелки. Хотя все примеры в Википедии кажутся против часовой стрелки.   -  person bradrn    schedule 20.08.2019


Ответы (2)


Давайте сначала посмотрим, как выглядят направления спирали:

R D L L U U R R R D D D L L L L U U U U ....

Мы можем разбить это на следующие последовательности:

      n times       n+1 times
       _^_           __^__
      /   \         /     \
R … R D … D L L … L U U … U
\_ _/       \__ __/
  v            v
n times     n+1 times

Мы можем повторить это, каждый раз увеличивая n на два, например:

data Dir = R | D | L | U

spiralSeq :: Int -> [Dir]
spiralSeq n = rn R ++ rn D ++ rn1 L ++ rn1 U
    where rn = replicate n
          rn1 = replicate (n + 1)

spiral :: [Dir]
spiral = concatMap spiralSeq [1, 3..]

Теперь мы можем использовать здесь Dir для вычисления следующей координаты, например:

move :: (Int, Int) -> Dir -> (Int, Int)
move (x, y) = go
    where go R = (x+1, y)
          go D = (x, y-1)
          go L = (x-1, y)
          go U = (x, y+1)

Мы можем использовать _7 _ для создания точек, например:

spiralPos :: [(Int, Int)]
spiralPos = scanl move (0,0) spiral

Это даст бесконечный список координат для спирали по часовой стрелке. Мы можем использовать _9 _ , чтобы взять первые k предметов:

Prelude> take 9 spiralPos
[(0,0),(1,0),(1,-1),(0,-1),(-1,-1),(-1,0),(-1,1),(0,1),(1,1)]
person Willem Van Onsem    schedule 20.08.2019
comment
Спасибо, рад, что это объяснило rn R ++ rn D ++ rn1 L ++ rn1 U так мысленно, что заставило меня понять, как это работает. Также разделение move концептуально очень помогло. - person cmdv; 20.08.2019
comment
@WillemVanOnsem Почему вы разделяете move на вспомогательную функцию go? Есть ли преимущества у этого подхода по сравнению с тем, чтобы делать это в одной функции, как в моем собственном ответе? - person bradrn; 21.08.2019
comment
@bradm: я не очень люблю писать (x, y) много раз :), так что нет, это скорее минимизация работы :) - person Willem Van Onsem; 21.08.2019

Идея следующего решения состоит в том, что вместо того, чтобы пытаться напрямую сгенерировать координаты, мы будем смотреть на направления от одной точки к другой. Если вы сделаете это, вы заметите, что, начиная с первой точки, мы идем 1 × вправо, 1 × вниз, 2 × влево, 2 × вверх, 3 × вправо, 3 × вниз, 4 × влево ... Затем они могут быть разделены на направление и количество повторений:

direction: > v < ^ > v < …
   # reps: 1 1 2 2 3 3 4 …

И это на самом деле дает нам два действительно простых шаблона! Направления просто меняются >, v, <, ^, >, в то время как количество повторений увеличивается на 1 каждые 2 раза. После того, как мы составили два бесконечных списка с этими шаблонами, их можно объединить вместе, чтобы получить общий список направлений >v<<^^>>>vvv<<<<…, который затем можно повторять для получения значений координат.

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


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

data Stream a = Stream a (Stream a) deriving (Show)

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

Затем я определяю тип направлений, а также функцию, определяющую, как они взаимодействуют с точками:

-- Note: I can’t use plain Left and Right
-- since they conflict with constructors
-- of the ‘Either’ data type
data Dir = LeftDir | RightDir | Up | Down deriving (Show)

type Point = (Int, Int)

move :: Dir -> Point -> Point
move LeftDir (x,y)  = (x-1,y)
move RightDir (x,y) = (x+1, y)
move Up (x,y)       = (x,y+1)
move Down (x,y)     = (x,y-1)

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

dirStream :: Stream Dir
dirStream = Stream RightDir $ Stream Down $ Stream LeftDir $ Stream Up dirVals

numRepsStream :: Stream Int
numRepsStream = go 1
  where
    go n = Stream n $ Stream n $ go (n+1)

На этом этапе нам понадобится функция для репликации каждого элемента потока определенное количество раз:

replicateS :: Stream Int -> Stream a -> Stream a
replicateS (Stream n ns) (Stream a as) = conss (replicate n a) $ replicateS ns as
  where
    -- add more than one element to the beginning of a stream
    conss :: [a] -> Stream a -> Stream a
    conss [] s = s
    conss (x:xs) s = Stream x $ appends xs s

Это дает replicateS dirStream numRepsStream для потока направлений. Теперь нам просто нужна функция для преобразования этих направлений в координаты, и мы решили проблему:

integrate :: Stream Dir -> Stream Point
integrate = go (0,0)
  where
    go p (Stream d ds) = Stream p (go (move d p) ds)

spiral :: Stream Point
spiral = integrate $ replicateS numRepsStream dirStream

К сожалению, печатать бесконечный поток несколько неудобно, поэтому для отладки и печати полезна следующая функция:

takeS :: Int -> Stream a -> [a]
takeS 0 _ = []; takeS n (Stream x xs) = x : (takeS (n-1) xs)
person bradrn    schedule 20.08.2019
comment
Спасибо за обстоятельный ответ, действительно здорово видеть ответ, использующий концепцию Streams и то, как проходил каждый этап :) - person cmdv; 20.08.2019
comment
Добро пожаловать, @cmdv! Насколько я знаю, потоки на самом деле не слишком интересны - это просто списки без конструктора [], что упрощает сопоставление некоторых шаблонов. Я также взглянул на другой ответ `@ WillemVanOnsem на этот вопрос, и похоже, что он использовал ту же идею, что и мое решение, но сделал это намного проще - напоминание о том, что создание решения из простых комбинаторов не обязательно лучше чем прямая рекурсия! - person bradrn; 21.08.2019