【问题标题】:Efficiently Following the Links in a Directed Graph (Common Lisp)有效地跟踪有向图中的链接(Common Lisp)
【发布时间】:2020-07-29 01:21:19
【问题描述】:

我想开发一种有效的策略,可以快速测试大型全连接有向标记图中是否存在预先指定的路径。例如,从某个节点开始,比如 node0,是否存在到另一个节点的路径,比如 node9,它遵循一系列标记的链接,比如 node0 -> link3 -> link1 -> link4+ -> link1 -> node9,其中link+ 表示该链接标签的一个或多个重复。该图是动态的,因此节点和链接将不断添加和删除。唯一节点和链接标签将是从底层语义信息构造的字符串。

我的第一个(最简单的)想法是将所有标记的图形节点和链接作为符号放在一个单独的包中。然后安装一个哈希表作为每个节点的符号值。哈希表将携带该节点的关联,从该节点发出的所有链接到它们各自的目标节点。测试链中的下一个链接是否存在,然后是一个简单的查表。查找的总数取决于链接链的长度。所有对节点和标签符号的编程引用都将通过包名称。

但是,我不确定将符号和符号值用作数据结构是否可取。在这种情况下,将它们放在自己的包中是否可以减轻潜在的冲突?

【问题讨论】:

    标签: common-lisp symbols


    【解决方案1】:

    如果你想使用符号,你不需要哈希表;您可以将数据存储在符号的symbol-value 插槽中,并将任何其他数据存储在其symbol-plist 中。查找要么在读取时已经完成,要么在运行时使用find-symbolintern。您可以使用unintern 将符号与其主包分离,但其他节点仍然可以引用它,因此您需要在删除节点时删除对该符号的任何其他引用(这就是为什么有时您同时存储传入和传出节点的边)。

    这是可以做到的,据我所知,这曾经是历史上处理符号的常用方法。一个可能的缺点是,当您创建一个包时,您必须为其命名(因此没有即时、匿名的包)。您必须可能选择一个当前未用作包名称的字符串,并将节点的名称限制为特定的包。

    实现这一点的另一种方法是拥有一个包含namenode 类,其中名称可以是用户选择的任何符号(在任何包中)。 graph 类维护所有节点和边等,您可以单独操作这些对象,而不会弄乱环境的包列表等。这可能会更干净一些。

    它最近才发布,所以我还想指出这本书的存在:Vsevolod Domkin 的Programming Algorithms,它使用 Common Lisp 来实现算法。

    【讨论】:

    • 嗯,看起来使用符号与节点的类/结构实例没有太大区别。使用符号 (gethash link node) 与使用节点实例 (gethash link (node-links node)) 进行查找。因此,正如您所注意到的,即使添加了间接,实例也应该更清晰。但是符号方法如何避免每个节点都使用哈希表来保存节点到下一个节点的传出链接呢?那将是一个很大的优势。期待Programming Algorithms
    • 例如(get symbol 'succ-nodes) => 后继节点列表
    • 是的,但是确定下一个后继节点取决于链接标签。似乎仍然需要每个符号的哈希表,在这种情况下在其属性列表中。
    • @davypough 是的,如果您需要按名称查找链接,属性列表可以包含一个哈希表
    【解决方案2】:

    与其痴迷于实现,我会设计一个系统需要遵循的协议。这是一个这样的(注意我在这里做了一些假设,其中一些可能是隐含的,并且没有一个可能与您希望事情的工作方式一致):

    ;;;; Protocol
    ;;;
    ;;; By assumption there is one link with each label, each link points
    ;;; at one other node.
    ;;;
    ;;; NODEs have identity and can be destructively modified but it is
    ;;; not specified whether node equality is object identity.
    ;;;
    
    (defgeneric node-link-labelled (node label)
      (:documentation "Return the node linked to NODE via LABEL, or NIL".))
    
    (defgeneric (setf node-link-labelled) (target node label)
      (:documentation "set the link with label LABEL of NODE to TARGET, replacing it if
    it exists.  Return TARGET."))
    
    (defgeneric nodes-equal (n1 n2)
      (:documentation "Are N1 and N2 the same node?"))
    
    (defgeneric node-remove-link (node label)
      (:documentation "Remove the link with label LABEL from NODE.  Return NODE.
    
    The link need not exist"))
    
    (defgeneric mapc-node-links (fn node)
      (:documentation "call FN with arguments NODE, LABEL TARGET for each link of NODE.
    
    FN is allowed to delete the link corresponding to LABEL but should not otherwise
    modify NODE"))
    

    然后你可以为这个协议编写实现。这是一个简单的节点是(<something> . <links>) 的conses。对于大量链接,这将很慢,但对于少量链接可能非常快。它有一个很好的特性,你可以给节点命名,这在上面的协议中是不支持的。

    ;;;; Consy nodes are simple
    ;;;
    
    (defun make-consy-node (&optional (label 'node))
      (list label))
    
    (defmethod node-link-labelled ((node cons) label)
      (cdr (assoc label (cdr node))))
    
    (defmethod nodes-equal ((n1 cons) (n2 cons))
      (eql n1 n2))
    
    (defmethod (setf node-link-labelled) (target (node cons) label)
      (let ((found (assoc label (cdr node))))
        (if found
            (setf (cdr found) target)
          (push (cons label target) (cdr node))))
      target)
    
    (defmethod node-remove-link ((node cons) label)
      (setf (cdr node) (delete-if (lambda (link)
                                    (eql (car link) label))
                                  (cdr node)))
      node)
    
    (defmethod mapc-node-links (fn (node cons))
      ;; This is at least safe
      (loop for (label . target) in (copy-list (cdr node))
            do (funcall fn node label target))
      node)
    

    或者您可以将节点实现为哈希表,这对于具有许多每个节点链接的图来说会很快:

    ;;;; Hashy nodes
    ;;;
    
    (defun make-hashy-node ()
      (make-hash-table))
    
    (defmethod nodes-equal ((n1 hash-table) (n2 hash-table))
      (eql n1 n2))
    
    (defmethod node-link-labelled ((node hash-table) label)
      (values (gethash label node nil)))
    
    (defmethod (setf node-link-labelled) (target (node hash-table) label)
      (setf (gethash label node) target)
      target)
    
    (defmethod node-remove-link ((node hash-table) label)
      (remhash label node)
      node)
    
    (defmethod mapc-node-links (fn (node hash-table))
      (maphash (lambda (label target)
                 (funcall fn node label target))
               node)
      node)
    

    或者你可以做很多其他的事情。由于它们都遵循协议,因此您可以混合使用它们:

    (let ((n1 (make-hashy-node)))
      (setf (node-link-labelled n1 'foo) (make-hashy-node)
            (node-link-labelled n1 'bar) (make-consy-node 'n2))
      n1)
    

    如果需要,您可以将节点构造定义为协议的一部分:

    (defgeneric make-node-of-sort (sort &key)
      (:documentation "make a node whose sort is SORT.  Methods on this GF should
    use EQL specializers on SORT"))
    
    ...
    
    (defmethod make-node-of-sort ((sort (eql 'consy)) &key (name 'node))
      (list name))
    
    ...
    

    【讨论】:

    • 感谢协议的快速启动。我应该能够或多或少地直接调整这个界面。
    猜你喜欢
    • 2012-01-20
    • 1970-01-01
    • 1970-01-01
    • 2022-11-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多