如何在Haskell中实现半边数据结构?

时间:2010-10-27 01:49:28

标签: data-structures haskell

有关数据结构的说明,请参阅

半边数据结构涉及周期。

  • 是否可以用Haskell等函数式语言实现它?
  • 是可变参考(STRef)要走的路吗?

由于

4 个答案:

答案 0 :(得分:2)

显然问题是半边参考下一个和相反的半边(其他参考没有问题)。你可以“打破循环”,例如通过不直接引用其他半边,而是仅引用ID(例如简单的Ints)。要按ID查找半边,可以将它们存储在Data.Map中。当然,这种方法需要一些记账以避免大毛乱,但这是我能想到的最简单的方法。

愚蠢的我,我不是在想懒惰。上面的解决方案适用于严格的函数式语言,但对Haskell来说是不必要的。

答案 1 :(得分:2)

为了有效地构建半边数据结构,您需要一个HE_vert的加速结构(让我们称之为HE_vert_acc ......但实际上你可以直接在HE_vert中执行此操作),它可以保存所有指向的HE_edges 此HE_vert。否则,在尝试定义" HE_edge *对时会出现非常糟糕的复杂性。 (例如,相反取向的相邻半边缘),例如,通过暴力比较。

因此,使用“绑结”方法可以轻松地为单个面创建半边数据结构,因为无论如何都(可能)没有对。但是如果你加入加速结构的复杂性来有效地决定这些对,那么它就会变得有点困难,因为你需要在不同的面上更新相同的 HE_vert_acc,然后将HE_edges更新为包含有效的一对。这些实际上是多个步骤。如何通过打结来将它们粘合在一起比构建循环双链表更复杂,而且不是很明显。

正因为如此......我不会对这个问题感到烦恼,如何在惯用的haskell"中构建这个数据结构。 我认为在尝试保持API功能的同时使用更多命令式方法是合理的。我可能会选择数组和状态monad。

并不是说打结是不可能的,但我还没有看到这样的实现。在我看来,这不是一个容易的问题。

编辑:所以我不能放弃并实现这一点,假设输入是一个.obj网格文件。

我的方法是基于这里描述的方法https://wiki.haskell.org/Tying_the_Knot#Migrated_from_the_old_wiki,但是来自Andrew Bromage的方法,他解释了为DFA打结而不知道编译时的结。

不幸的是,半边数据结构甚至更复杂,因为它实际上由3个数据结构组成。

所以我从我真正想要的东西开始:

data HeVert a = HeVert {
    vcoord  :: a        -- the coordinates of the vertex
  , emedge  :: HeEdge a -- one of the half-edges emanating from the vertex
}

data HeFace a = HeFace {
  bordedge :: HeEdge a -- one of the half-edges bordering the face
}

data HeEdge a = HeEdge {
    startvert :: HeVert a          -- start-vertex of the half-edge
  , oppedge   :: Maybe (HeEdge a)  -- oppositely oriented adjacent half-edge
  , edgeface  :: HeFace a          -- face the half-edge borders
  , nextedge  :: HeEdge a          -- next half-edge around the face
}

问题是我们在构建它时遇到了多个问题,因此对于所有这些数据结构,我们将使用"间接"一个基本上只保存.obj网格文件给出的明文信息。

所以我想出了这个:

data IndirectHeEdge = IndirectHeEdge {
    edgeindex  :: Int  -- edge index
  , svindex    :: Int  -- index of start-vertice
  , nvindex    :: Int  -- index of next-vertice
  , indexf     :: Int  -- index of face
  , offsetedge :: Int  -- offset to get the next edge
}

data IndirectHeVert = IndirectHeVert {
    emedgeindex  :: Int    -- emanating edge index (starts at 1)
  , edgelist     :: [Int]  -- index of edge that points to this vertice
}

data IndirectHeFace =
  IndirectHeFace (Int, [Int]) -- (faceIndex, [verticeindex])

有些事情可能不直观,可以做得更好,例如" offsetedge"事情。 看看我是如何在任何地方保存实际顶点的。这只是很多索引的东西,它们模仿C指针。 我们需要" edgelist"以后有效地找到相反方向的相邻半边缘。

我没有详细说明如何填充这些间接数据结构,因为这确实特定于.obj文件格式。我只是举例说明事情是如何转变的。

假设我们有以下网格文件:

v 50.0 50.0
v 250.0 50.0
v 50.0 250.0
v 250.0 250.0
v 50.0 500.0
v 250.0 500.0
f 1 2 4 3
f 3 4 6 5

间接面孔现在看起来像这样:

[IndirectHeFace (0,[1,2,4,3]),IndirectHeFace (1,[3,4,6,5])]

间接边缘:

[IndirectHeEdge {edgeindex = 0, svindex = 1, nvindex = 2, indexf = 0, offsetedge = 1},
IndirectHeEdge {1, 2, 4, 0, 1},
IndirectHeEdge {2, 4, 3, 0, 1},
IndirectHeEdge {3, 3, 1, 0, -3},
IndirectHeEdge {0, 3, 4, 1, 1},
IndirectHeEdge {1, 4, 6, 1, 1},
IndirectHeEdge {2, 6, 5, 1, 1},
IndirectHeEdge {3, 5, 3, 1, -3}]

间接顶点:

[(1,IndirectHeVert {emedgeindex = 0, edgelist = [3]}),
(2,IndirectHeVert {1, [0]}),
(3,IndirectHeVert {4, [7,2]}),
(4,IndirectHeVert {5, [4,1]}),
(5,IndirectHeVert {7, [6]}),
(6,IndirectHeVert {6, [5]})]

现在真正有趣的部分是我们如何将这些间接数据结构转变为" direct"我们在一开始就定义了一个。这有点棘手,但基本上只是索引查找而且因为懒惰而起作用。

这里是伪代码(实际实现不仅使用列表而且还有额外的开销以使函数安全):

indirectToDirect :: [a]   -- parsed vertices, e.g. 2d points (Double, Double)
                 -> [IndirectHeEdge]
                 -> [IndirectHeFace]
                 -> [IndirectHeVert]
                 -> HeEdge a
indirectToDirect points edges faces vertices
  = thisEdge (head edges)
  where
    thisEdge edge
      = HeEdge (thisVert (vertices !! svindex edge) $ svindex edge)
               (thisOppEdge (svindex edge) $ indexf edge)
               (thisFace $ faces !! indexf edge)
               (thisEdge $ edges !! (edgeindex edge + offsetedge edge))
    thisFace face = HeFace $ thisEdge (edges !! (head . snd $ face))
    thisVert vertice coordindex
      = HeVert (points !! (coordindex - 1))
               (thisEdge $ points !! (emedgeindex vertice - 1))
    thisOppEdge startverticeindex faceindex
      = thisEdge
        <$>
        (headMay
          . filter ((/=) faceindex . indexf)
          . fmap (edges !!)
          . edgelist         -- getter
          $ vertices !! startverticeindex)

请注意,我们无法真正让这一回归成为&#34;可能(HeEdge a)&#34;因为它会尝试评估整个事物(这是无限的),以便知道使用哪个构造函数。 我不得不为每个人添加NoVert / NoEdge / NoFace构造函数以避免&#34;可能&#34;。

另一个缺点是,这在很大程度上取决于输入,并不是真正的通用库。我也不完全确定它是否会重新评估(这仍然非常便宜)已经访问过的边缘。

使用Data.IntMap.Lazy似乎可以提高性能(至少对于IndirectHeVert列表而言)。 Data.Vector在这里对我没有多大帮助。

除非你想使用数组或向量,否则不需要在任何地方使用状态monad。

答案 2 :(得分:1)

如果有问题的任务允许你构建一次半边结构,然后多次查询,那么就像在评论和其他答案中指出的那样,懒惰的知道方法是要走的路。

但是,如果要更新结构,那么纯功能界面可能会很麻烦。此外,您还需要考虑更新函数的O(..)要求。事实上,你可能需要可变的内部表示(可能在顶部使用纯API)。

答案 3 :(得分:0)

我为这类事情遇到了一个有用的多态性应用程序。您通常需要静态非无限版本进行序列化,以及内部表示的结合版本。

如果您创建一个多态的版本,那么您可以使用记录语法更新该特定值:

data Foo edge_type_t = Depot {
   edge_type :: edge_type_t,
   idxI, idxE, idxF, idxL :: !Int
} deriving (Show, Read)

loadFoo edgetypes d = d { edge_type = edgetypes ! edge_type d }
unloadFoo d = d { edge_type = edgetype_id $ edge_type d }

然而有一个重要的警告:You cannot make a Foo (Foo (Foo( ...))) type this way因为Haskell必须递归地理解类型。 :(