Scala 使用严格的函数编程从偏序集生成DAG

Scala 使用严格的函数编程从偏序集生成DAG,scala,functional-programming,graph-theory,directed-acyclic-graphs,partial-ordering,Scala,Functional Programming,Graph Theory,Directed Acyclic Graphs,Partial Ordering,这里是我的问题:我有一个序列S(非空但可能不是不同的)集S_I,对于每个S_,我需要知道S(I)中有多少集S_j≠ j) 是s_i的子集 我还需要增量性能:一旦我拥有了所有计数,我就可以用一组s_I的子集替换一组s_I,并增量更新计数 使用纯函数式代码执行所有这些将是一个巨大的优势(Scala中的I代码) 由于集合包含是一种偏序,我认为解决我的问题的最佳方法是构建一个DAG,该DAG表示集合的Hasse图,边表示包含,并将一个整数值连接到每个节点,表示节点下方的子DAG的大小加1。然而,我已经被

这里是我的问题:我有一个序列S(非空但可能不是不同的)集S_I,对于每个S_,我需要知道S(I)中有多少集S_j≠ j) 是s_i的子集

我还需要增量性能:一旦我拥有了所有计数,我就可以用一组s_I的子集替换一组s_I,并增量更新计数

使用纯函数式代码执行所有这些将是一个巨大的优势(Scala中的I代码)

由于集合包含是一种偏序,我认为解决我的问题的最佳方法是构建一个DAG,该DAG表示集合的Hasse图,边表示包含,并将一个整数值连接到每个节点,表示节点下方的子DAG的大小加1。然而,我已经被困了好几天,试图开发从偏序构建哈斯图的算法(让我们不要谈论增量!),尽管我认为这将是一些标准的本科生材料

以下是我的数据结构:

case class HNode[A] (
  val v: A,
  val child: List[HNode[A]]) {
  val rank = 1 + child.map(_.rank).sum
}
我的DAG由根列表和一些偏序定义:

class Hasse[A](val po: PartialOrdering[A], val roots: List[HNode[A]]) {
  def +(v: A): Hasse[A] = new Hasse[A](po, add(v, roots))

  private def collect(v: A, roots: List[HNode[A]], collected: List[HNode[A]]): List[HNode[A]] =
    if (roots == Nil) collected
    else {
      val (subsets, remaining) = roots.partition(r => po.lteq(r.v, v))
      collect(v, remaining.map(_.child).flatten, subsets.filter(r => !collected.exists(c => po.lteq(r.v, c.v))) ::: collected)
    }
}
我被困在这里了。我最后一次向DAG添加一个新值v是:

  • 在DAG中查找v的所有“根子集”rs_i,即v的子集,使得rs_i的超集不是v的子集。这可以通过在图形上执行搜索(BFS或DFS)来轻松完成(
    collect
    函数,可能是非最优的,甚至是有缺陷的)
  • 构建新节点n_v,其子节点是以前找到的rs_i
  • 现在,让我们找出n_v应该附加在哪里:对于给定的根列表,找出v的超集。如果没有找到,则将n_v添加到根中,并从根中删除n_v的子集。否则,对超集的子级递归执行步骤3

  • 我还没有完全实现这个算法,但对于我这个看似简单的问题来说,它似乎是不必要的循环进化和非最优的。是否有更简单的算法可用(Google对此一无所知)?

    假设您的DAG
    G
    为每个集合包含一个节点
    v
    ,属性为
    v.s
    (集合)和
    v.count
    (集合的实例数),包括一个带有
    G.root.s=所有集合的并集的节点
    G.root
    (其中
    G.root.count=0
    如果此集合从未出现在您的集合中)

    然后,要计算
    s
    的不同子集的数量,您可以执行以下操作(在Scala、Python和伪代码的混搭中):

    在哪里

    get_subsets(s, v) :
       if(v.s is not a subset of s, {}, 
          union({v} :: apply(v.children, lambda x: get_subsets(s, x))))
    

    不过,在我看来,出于性能原因,最好放弃这种纯粹的功能解决方案……它在列表和树上运行良好,但除此之外,事情变得很艰难。

    经过一些工作后,我终于按照我最初的直觉解决了我的问题。收集方法和排名评估有缺陷,我重新开始使用尾部递归编写它们作为奖励。以下是我获得的代码:

    final case class HNode[A](
      val v: A,
      val child: List[HNode[A]]) {
      val rank: Int = 1 + count(child, Set.empty)
    
      @tailrec
      private def count(stack: List[HNode[A]], c: Set[HNode[A]]): Int =
        if (stack == Nil) c.size
        else {
          val head :: rem = stack
          if (c(head)) count(rem, c)
          else count(head.child ::: rem, c + head)
        }
    }
    
    // ...
    
      private def add(v: A, roots: List[HNode[A]]): List[HNode[A]] = {
        val newNode = HNode(v, collect(v, roots, Nil))
        attach(newNode, roots)
      }
    
      private def attach(n: HNode[A], roots: List[HNode[A]]): List[HNode[A]] =
        if (roots.contains(n)) roots
        else {
          val (supersets, remaining) = roots.partition { r =>
            // Strict superset to avoid creating cycles in case of equal elements
            po.tryCompare(n.v, r.v) == Some(-1)
          }
          if (supersets.isEmpty) n :: remaining.filter(r => !po.lteq(r.v, n.v))
          else {
            supersets.map(s => HNode(s.v, attach(n, s.child))) ::: remaining
          }
        }
    
      @tailrec
      private def collect(v: A, stack: List[HNode[A]], collected: List[HNode[A]]): List[HNode[A]] =
        if (stack == Nil) collected
        else {
          val head :: tail = stack
    
          if (collected.exists(c => po.lteq(head.v, c.v))) collect(v, tail, collected)
          else if (po.lteq(head.v, v)) collect(v, tail, head :: (collected.filter(c => !po.lteq(c.v, head.v))))
          else collect(v, head.child ::: tail, collected)
        }
    
    我现在必须检查一些优化: -在收集子集时,切断具有完全不同集合的分支(如Rex Kerr所建议的) -查看按大小对集合进行排序是否可以改进过程(如mitchus所建议的)

    下面的问题是计算add()操作的(最坏情况)复杂性。 n是集合的数量,d是最大集合的大小,复杂度可能是O(n²d),但我希望它能被细化。我的推理是:如果所有集合都是不同的,DAG将被简化为一个根/叶序列。因此,每次我尝试向数据结构中添加节点时,我仍然必须检查是否包含已存在的每个节点(在收集和附加过程中)。这将导致1+2+…+n=n(n+1)/2∈ O(n²)包含检查


    每个集合包含测试都是O(d),因此结果是。

    该算法对我来说非常简单,没有不必要的卷积。到底是什么问题?它的Scala代码几乎不会比您描述的长。(尽管我认为您甚至没有完全描述它。)好吧,因为我已经开始了函数编程(~6个月前),在处理递归数据结构时,我已经习惯了一行程序。开发一个三步算法感觉很尴尬,它不存在于一个递归调用中(步骤1.与步骤3断开)。此外,该算法会检查两次子集(步骤1和步骤3),这感觉是错误的。作为参考,我最近实现了一个二项式堆,这感觉简单多了(尽管可能是因为算法定义得更好)。您有两件本质上不同的事情要做:如果合适,将新集合添加为根节点,并将其粘贴到子列表中,然后构建合适的子列表(至少有一件事,可能有两件)。将所有这些都放在一行合理长度的行中似乎非常乐观。事实上,我在以前错误的分析中成功地做到了这一点,我发现偏序会导致树。我认为用DAG替换树很容易,该死,我错了:偏序意味着我的新元素的子集可以显示为在DAG中,不仅仅是在一个特定的子树中。这个答案假设DAG存在,不是吗?我的第一个问题是从偏序生成DAG。经过进一步的研究,似乎我想计算传递闭包的倒数,可能与拓扑排序有关。实际上,我所拥有的只是p艺术顺序。问题的根源在于,我没有孩子。我希望尽可能有效地找到孩子(我希望比O(n²)更好)的确是的,在这里我假设DAG已经存在。要构建DAG,第一步可以按大小对集合进行排序;子集总是比超集小。下一步,我将构建一个人工根节点,集合=所有集合的并集。然后,按照大小递减的顺序获取集合,为其创建一个节点,然后进行决策
    final case class HNode[A](
      val v: A,
      val child: List[HNode[A]]) {
      val rank: Int = 1 + count(child, Set.empty)
    
      @tailrec
      private def count(stack: List[HNode[A]], c: Set[HNode[A]]): Int =
        if (stack == Nil) c.size
        else {
          val head :: rem = stack
          if (c(head)) count(rem, c)
          else count(head.child ::: rem, c + head)
        }
    }
    
    // ...
    
      private def add(v: A, roots: List[HNode[A]]): List[HNode[A]] = {
        val newNode = HNode(v, collect(v, roots, Nil))
        attach(newNode, roots)
      }
    
      private def attach(n: HNode[A], roots: List[HNode[A]]): List[HNode[A]] =
        if (roots.contains(n)) roots
        else {
          val (supersets, remaining) = roots.partition { r =>
            // Strict superset to avoid creating cycles in case of equal elements
            po.tryCompare(n.v, r.v) == Some(-1)
          }
          if (supersets.isEmpty) n :: remaining.filter(r => !po.lteq(r.v, n.v))
          else {
            supersets.map(s => HNode(s.v, attach(n, s.child))) ::: remaining
          }
        }
    
      @tailrec
      private def collect(v: A, stack: List[HNode[A]], collected: List[HNode[A]]): List[HNode[A]] =
        if (stack == Nil) collected
        else {
          val head :: tail = stack
    
          if (collected.exists(c => po.lteq(head.v, c.v))) collect(v, tail, collected)
          else if (po.lteq(head.v, v)) collect(v, tail, head :: (collected.filter(c => !po.lteq(c.v, head.v))))
          else collect(v, head.child ::: tail, collected)
        }