【问题标题】:How to ensure correct edges in graph如何确保图中的正确边缘
【发布时间】:2015-05-28 07:18:29
【问题描述】:

我试图在 Haskell 中为图表创建一个数据类型,如下所示:

type Vertex a = a
type Edge a = (Vertex a, Vertex a)
data Graph a = Combine [Vertex a] [Edge a]

这是一种适用于我想做的表示,但我意识到可能存在不在顶点列表中的顶点的边。

因此,我的问题是是否有可能确保每条边仅包含顶点列表中的顶点?

我已经想了很多,但到目前为止我得到的最好的想法是创建一个新图,其中边缘列表中所有缺失的顶点都添加到顶点列表中。比如:

fix_graph :: Graph a -> Graph a
fix_graph (Combine v e) = Combine (removeDuplicates (v ++ [x | (x,_) <- e] ++ [y | (_,y) <- e])) e

removeDuplicates :: [t] -> [t]
...

因为这个想法并没有真正让我满意(也因为我没有花时间很好地实现它),我想知道是否有可能有一个数据构造函数来添加不在边缘的顶点顶点列表还立即。

我已经阅读了here 的答案,但我不太喜欢那里使用的邻接表示。我知道我很烦人,但我只是想知道是否没有其他可能性可以解决这个问题。

如果有人可以帮助我解决问题或摆脱我的幻想,那将很有帮助...

提前致谢

【问题讨论】:

  • 您可以为此定义一个smart constructor;而不是导出你的 Combine 值构造函数,只向用户公开那个智能构造函数。
  • 最好(至少尝试)以这样的方式设计您的数据结构,以便在任何时候都不能构造无效的数据结构,这样就不需要“修复”它们;但如果没有其他方法,隐藏构造函数并确保从模块中只返回有效值是下一个最佳方法 - 另请参阅 John de Goes 关于智能数据与智能构造函数的精彩文章:degoes.net/articles/newtypes-suck跨度>
  • 另外,我认为在您设计要重用的代码时,类型别名并不是确保类型健全性的好方法。
  • @Jubobs 这确实是我想要的!您能否将其包装在答案中,以便我接受?
  • @ErikAllik 恐怕我对 Haskell 的了解还不够好,还无法理解那里的设计问题。另一方面,我也不喜欢“修复”,这就是为什么我要求其他可能的解决方案。

标签: haskell graph types


【解决方案1】:

所以有几个不同的选择:

喜结连理

在 Haskell 中有很多方法可以对图形进行编码。最简单的方法是使用称为“打结”的过程在树数据结构中创建循环。例如,对这个图进行编码:

                  .--.
  A -- B -- C -- D -./
  |    |    |    |
  E -- F    G -- H
  |              |
  +--------------+

您可以简单地将节点写为其名称和子节点列表:

data Node = Node String [Node]
instance Eq Node where
    Node a _ == Node b _ = a == b

my_graph = [a, b, c, d, e, f, g, h] where
    a = Node "A" [b, e]
    b = Node "B" [a, c, f]
    c = Node "C" [b, d, g]
    d = Node "D" [c, d, h]
    e = Node "E" [a, f, h]
    f = Node "F" [b, e]
    g = Node "G" [c, h]
    h = Node "H" [d, e, g]

这有很多方便:您现在可以像任何其他 Haskell 数据结构一样使用尾递归函数遍历数据结构。要终止循环,请在递归时将当前项目附加到 path 变量上,递归逻辑应该说的第一件事是 | nodeelempath = ... 以随心所欲地处理循环。

另一方面,您的次要一致性问题已经演变成非常棘手的一致性问题。例如考虑这两者之间的区别:

-- A has one child, which is itself; B has one child, which is A.
let a = Node "A" [a]; b = Node "B" [a] in [a, b]

-- this looks almost the same but if you descend far enough on B you find an
-- impostor node with the wrong children.
let a = Node "A" [a]
    impostor = Node "A" [b]
    b = Node "B" [Node "A" [Node "A" [impostor]]] 
in [a, b]

所以这很糟糕,我唯一真正的答案是,“通过转换为以下之一来标准化......”。

无论如何,上面的技巧也被称为 mutual recursionletrec,基本上意味着在 wherelet 子句中,所有你放在那里的定义可以“看到对方”。这不是懒惰的属性;你也可以用严格的语言制作上述数据结构——但是函数式严格语言的语言设计理解这种方式的相互递归定义可能有点困难. (使用非函数式语言,您只需根据需要创建指针。)

显式命理

现在想一想您将如何获取我们上面的图表,并将其转换为您的表示形式。最简单的方法是通过包含Array 的中间人步骤:

import From.Above.Code (Node)
import Data.Array

type Graph = Array [Int]

graph :: [Node] -> Maybe Graph
graph nodes = fmap (array (1, length nodes)) . sequence $ map format nodes where
    indices = zip nodes [1..]
    pair x y = (x, y)
    format node@(Node _ children) = do -- in the Maybe monad
        index <- lookup node indices
        index_list <- sequence $ map (flip lookup indices) children
        return (index, index_list)

现在,一致性问题要少得多,现在都可以通过编程方式加以缓解。但是,如果您想以编程方式使用 State monad 创建这样的图,并且您希望暂时使数据结构处于不一致的状态,直到读取正确的节点,这些一致性问题可能会起到一定的作用。唯一的缺点是当你将图表写入文件时,它看起来有点难以理解,因为数字不如字符串友好:

 array (1, 8) [
     (1, [2, 5]),
     (2, [1, 3, 6]),
     (3, [2, 4, 7]),
     (4, [3, 4, 8]),
     (5, [1, 6, 8]),
     (6, [2, 5]),
     (7, [3, 8]),
     (8, [4, 5, 7])]

您可以使用Map String [String] 来解决此问题,以权衡访问变为O(log n)。在任何情况下,您都应该了解该表示:您将希望转换为 IntMap [Int],然后在执行您提议的“完整性检查”时返回。

一旦你有了这些,事实证明你可以使用支持Array Int Node来创建递归[Node],如上:

nodesFromArray arr = nodes where
    makeNode index children = Node (show index) [backingArray ! c | c <- children]
    backingArray = array (bounds arr) [(i, makeNode i c) | (i, c) <- assocs arr]
    nodes = map makeNode arr

边列表

一旦你有了上面的列表(Map.toList 或 Array.assocs),边列表就变得很容易了:

edges_from_array = concatMap . uncurry (fmap . pair) . assocs

另一面稍微复杂一些,可以直接完成你想要做的事情:

import Data.Map (Map)
import Data.Set (Set)
import qualified Data.Map as Map
import qualified Data.Set as Set

makeGraphMap vertices edges = add edges (Map.fromList $ blankGraph vertices) where
     blankGraph verts = zip verts (repeat Set.empty)
     setInsert x Nothing = Just $ Set.singleton x
     setInsert x (Just set) = Just $ Set.insert x set
     add [] graphMap = fmap Set.toList graphMap
     add ((a, b) : es) graphMap = Map.alter (setInsert b) a verts

也就是说,我们使用将键映射到其子集的映射来遍历边列表;我们将其初始化为映射到空集的顶点列表(这样我们就可以在图中断开连接的单个节点),然后通过将值插入键的集合来遍历边,如果我们不创建该集合看钥匙。

【讨论】:

  • 您的回答肯定是彻底的,但恐怕它包含太多我还不知道的元素,智能构造函数对我来说似乎是一种更优雅的解决方案。你有没有提到智能构造函数的原因(你的解决方案有优势吗)?
  • 我的意思是,我发现智能构造函数非常有用,但有某些理由更喜欢接近“每个不同的数据只有一个表示”的情况。有时,它就像能够进行模式匹配一​​样简单,尽管这在此处受到限制——或者拥有deriving (Eq) 也很好。对于修改您的文件的任何人来说,一致性仍然是一个问题——这确实发生了!如果没有模式匹配和记录语法,修改结构需要编写修改 API,只能通过导入依赖项来标准化(想想Control.LensData.Traversable)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-07-18
  • 1970-01-01
  • 2018-05-06
  • 2022-12-16
相关资源
最近更新 更多