有关数据结构的说明,请参阅
半边数据结构涉及周期。
由于
答案 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必须递归地理解类型。 :(