Dependencies 依赖子图的拓扑序

Dependencies 依赖子图的拓扑序,dependencies,graph-algorithm,topological-sort,subgraph,Dependencies,Graph Algorithm,Topological Sort,Subgraph,我正在寻找一种标准拓扑排序算法的变体,该算法在节点子集上运行 考虑一个带有三种有向边的标记节点图:“依赖”、“之前”和“之后” 我想要的函数接受节点子集并返回线性排序。线性排序遵循“之前”和“之后”约束,以及将“依赖”视为“之前”约束。线性排序中的节点应该是输入节点的超集,以便包含依赖项 示例图: A depends on B B depends on C D before C E after C f({A}) -> [C B A] f({A D}) -> [D C B

我正在寻找一种标准拓扑排序算法的变体,该算法在节点子集上运行

考虑一个带有三种有向边的标记节点图:“依赖”、“之前”和“之后”

我想要的函数接受节点子集并返回线性排序。线性排序遵循“之前”和“之后”约束,以及将“依赖”视为“之前”约束。线性排序中的节点应该是输入节点的超集,以便包含依赖项

示例图:

A depends on B
B depends on C
D before C
E after C
f({A})     -> [C B A]
f({A D})   -> [D C B A]
f({B D E}) -> [D C B E] or [D C E B]
Y之后的X可以被简单地重写为X之前的Y

测试用例:

A depends on B
B depends on C
D before C
E after C
f({A})     -> [C B A]
f({A D})   -> [D C B A]
f({B D E}) -> [D C B E] or [D C E B]

优点:还可以将算法配置为强制排序中的第一个和最后一个节点。

获取与感兴趣节点及其依赖项的并集相交的整个图的拓扑排序。在伪代码中:

λ N = A ∩ (N ∪ D)
其中A是拓扑排序图的有序集,N是您关心的节点子集,D是N的依赖项。请注意,相交运算符必须尊重A的顺序

或者在Haskell中(使用数字作为节点,而不是字母,如您的示例所示):


这假设您可以指定图形中的所有边。请注意,您只需要计算一次总排序,因此它应该在后续调用中执行

上述代码将输出:

> f [0]
[2,1,0]

> f [0, 3]
[3,2,1,0]

> f [1, 3, 4]
[3,2,4,1]
这与您上面的测试用例相匹配

如果由于某种原因,不能指定图形中的每条边,而只能指定相对约束,请计算(N∪ D) 如上所述,应用约束满足。实现这一点的简单方法是尝试这些节点的每个排列,直到找到一个满足所有约束的排列。显然,即使是简单的“深度优先”和“回溯跟踪”方法,您也可以更有效地完成此任务


编辑:深度优先代码

很简单。我们创建一个包含所有我们关心的节点排列的树,然后 遍历/修剪该树,直到找到满足所有约束的排列(注意,我们将依赖项附加到约束中,因为依赖项也是约束)。所有约束条件均以(A,B)形式指定,表示“A必须在B之后”

由于我们将排列生成为一棵树,而不是一个列表,因此只要给定的路径前缀违反约束,我们就可以轻松地修剪搜索空间的大块

import Data.Maybe (fromMaybe, isJust)
import Data.List (union, nub, elemIndex, find)
import Data.Tree (unfoldTree, Tree (Node))
import Control.Applicative (liftA2)

dependencies = [(0, 1), (1, 2)]

constraints = [(2, 3), (4, 2)] ++ dependencies

f nodes = search $ permutationsTree $ (deps `union` nodes)
  where deps = nub $ concatMap dependenciesOf nodes

search (Node path children)
  | satisfies && null children = Just path
  | satisfies = fromMaybe Nothing $ find isJust $ map search children
  | otherwise = Nothing
  where satisfies   = all (isSatisfied path) constraints
        constraints = constraintsFor path

constraintsFor xs = filter isApplicable constraints
  where isApplicable (a, b) = (a `elem` xs) && (b `elem` xs)

isSatisfied path (a, b) = fromMaybe False $ liftA2 (>) i1 i2
  where i1 = a `elemIndex` path
        i2 = b `elemIndex` path 

permutationsTree xs = unfoldTree next ([], xs)
  where next (path, xs)      = (path, map (appendTo path) (select xs))
        appendTo path (a, b) = (path ++ [a], b)

select [] = []
select (x:xs) = (x, xs) : map (fmap (x:)) (select xs)

dependenciesOf x = nub $ x : concatMap dependenciesOf deps
  where deps = map snd $ filter (\(a, b) -> a == x) dependencies
大部分代码都相当直截了当,但下面是我脑海中的一些注释

  • 在计算上,这比之前发布的算法要昂贵得多。即使使用更复杂的约束解算器,您也不可能做得更好(因为对于这样的约束,您实际上无法进行任何预计算……至少没有一个对我来说是显而易见的)

  • “f”函数返回一个Maybe,因为可能没有满足所有指定约束的路径

  • constraintsFor大约占总计算时间的43%。这很幼稚。我们可以做一些事情来加快速度:

    1) 一旦路径满足约束,向其添加节点不会使其违反该约束,但我们不会利用这一事实。相反,我们只是不断地对给定路径的所有相关约束进行重新测试,即使已知该约束以前已通过

    2) 我们对约束进行线性搜索,以确定哪些约束是适用的。相反,如果我们将它们索引到它们应用的节点,我们可以加快速度

    3) 减少要测试的约束数量显然也会减少ISsatified调用,这大约占计算时间的25%

  • 如果要在严格执行的环境中实现这样的代码,则必须稍微修改代码结构。实际上,这段代码很大程度上依赖于排列树的惰性,这使得我们不必将搜索代码与树生成代码纠缠在一起,因为搜索代码不会沿着它认为不合适的路径前进

最后,如果要查找所有解决方案,而不是仅查找第一个,只需将搜索正文更改为:

| satisfies && null children = [path]
| satisfies = concatMap search children
| otherwise = []

我没有花任何时间优化这段代码或类似的东西,仅仅是因为假设您能够指定完整的图(我相信您可以),原始算法显然是优越的。

“注意,您只需要计算一次总排序”这是我缺少的洞察力,因为我正在寻找一种避免访问每个节点的方法。但是,我的图形是在程序初始化期间建立的,因此只要拓扑排序被延迟并缓存,它就应该是好的。谢谢这就是说,如果你更新了你的答案,加入了一些深度优先和回溯跟踪方法的代码,你会非常棒。添加了深度优先代码。另外,还有一个关于原始代码的简要说明。在实践中,我会将排序转换为Node->Rank的映射,然后根据它们的级别对(节点
union
deps)进行排序。当您关心的节点数远小于图形中的总节点数时,这将大大加快速度。请参见此处的参考实现: