【问题标题】:Coordinates for clockwise outwards spiral顺时针向外螺旋的坐标
【发布时间】:2019-12-25 10:24:45
【问题描述】:

我正在尝试使用 Haskell 制作我认为称为 Ulam 螺旋的东西。 它需要顺时针向外旋转:

   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)

我见过Looping in a spiral 解决方案,但是这是逆时针方向的,它的输入需要矩阵的大小。

我还发现this 代码可以满足我的需要,但它似乎是逆时针方向的,向上而不是顺时针向右步进 :(

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)

经过大量实验后,我似乎无法更改旋转或开始步进方向,有人可以指出我正确的方式或不使用列表理解的解决方案,因为我发现它们很难解码?

【问题讨论】:

  • 提示:如何将螺旋从逆时针变为顺时针?
  • 首先我会尝试用英语和/或数学写下坐标序列应该是什么或如何产生它。一旦你有了这些,你就可以将一些东西翻译成代码。
  • 顺时针方向,我刚刚在 FP slack 频道上获得了一个很好的解决方案,所以如果 OP 没有时间,我可以在这里发布 :)
  • 旁注:乌拉姆螺旋特指a spiral highlighting the primes,无论是顺时针还是逆时针。不过维基百科上的例子似乎都是逆时针的。

标签: haskell recursion spiral


【解决方案1】:

让我们先来看看螺旋线的方向是怎样的:

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 增加 2,例如:

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)

我们可以使用scanl :: (a -&gt; b -&gt; a) -&gt; a -&gt; [b] -&gt; [a]来生成积分,比如:

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

这将为顺时针螺旋生成一个无限的坐标列表。我们可以使用take :: Int -&gt; [a] -&gt; [a] 来获取前 k 个项目:

Prelude> take 9 spiralPos
[(0,0),(1,0),(1,-1),(0,-1),(-1,-1),(-1,0),(-1,1),(0,1),(1,1)]

【讨论】:

  • 谢谢,很高兴这解释了rn R ++ rn D ++ rn1 L ++ rn1 U,因为这让我对它的工作原理感到震惊。在概念上拆分 move 也很有帮助。
  • @WillemVanOnsem 为什么将move 拆分为辅助函数go?与在我自己的答案中那样在一个函数中执行此方法相比,这种方法有什么优势吗?
  • @bradm:我不太喜欢写(x, y) 很多次 :),所以不,它更减少工作:)
【解决方案2】:

以下解决方案的想法是,我们不会尝试直接生成坐标,而是查看从一点到另一点的方向。如果你这样做,你会注意到从第一点开始,我们走 1× 右,1× 下,2× 左,2× 上,3× 右,3× 下,4× 左……然后这些可以是分为方向重复次数

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

这实际上给了我们两个非常直接的模式!方向只是旋转 &gt;v&lt;^&gt;,而代表次数每 2 次增加 1。一旦我们用这些模式创建了两个无限列表,就可以将它们组合在一起以获得方向的整体列表&gt;v&lt;&lt;^^&gt;&gt;&gt;vvv&lt;&lt;&lt;&lt;…,然后可以对其进行迭代以获得坐标值。

现在,我一直认为仅仅给某人一堆代码作为解决方案并不是最好的学习方式,所以我高度鼓励你自己尝试实现上述想法在下面查看我的解决方案。


欢迎回来(如果您确实尝试自己实现它)。现在:我自己的解决方案。首先,我为无限流定义了一个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)

【讨论】:

  • 感谢您的详尽回答,看到使用 Streams 概念的答案以及每个阶段是如何实现的真的很酷:)
  • 不客气@cmdv!据我所知,流并不是很有趣——它们只是没有[] 构造函数的列表,这使得一些模式匹配更容易一些。我还看了 `@WillemVanOnsem 对这个问题的其他回答,看起来他使用了与我的解决方案相同的想法,但他做得更简单——提醒从简单的组合器构建解决方案不一定更好比直接递归!
猜你喜欢
  • 2014-10-03
  • 2021-08-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-12-07
  • 1970-01-01
相关资源
最近更新 更多