Scala 使用严格的函数编程从偏序集生成DAG
这里是我的问题:我有一个序列S(非空但可能不是不同的)集S_I,对于每个S_,我需要知道S(I)中有多少集S_j≠ j) 是s_i的子集 我还需要增量性能:一旦我拥有了所有计数,我就可以用一组s_I的子集替换一组s_I,并增量更新计数 使用纯函数式代码执行所有这些将是一个巨大的优势(Scala中的I代码) 由于集合包含是一种偏序,我认为解决我的问题的最佳方法是构建一个DAG,该DAG表示集合的Hasse图,边表示包含,并将一个整数值连接到每个节点,表示节点下方的子DAG的大小加1。然而,我已经被困了好几天,试图开发从偏序构建哈斯图的算法(让我们不要谈论增量!),尽管我认为这将是一些标准的本科生材料 以下是我的数据结构: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。然而,我已经被
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是:
collect
函数,可能是非最优的,甚至是有缺陷的)我还没有完全实现这个算法,但对于我这个看似简单的问题来说,它似乎是不必要的循环进化和非最优的。是否有更简单的算法可用(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)
}