我正在寻找标准拓扑排序算法的变体,该算法在节点子集上运行。
考虑带有三种有向边的标记节点图:“依赖于”,“之前”和“之后”。
我想要的函数接受节点的子集并返回线性排序。线性排序服从“前”和“后”约束,以及将“依赖”视为“之前”约束。线性排序中的节点应该是输入节点的超集,以便包含依赖关系。
示例图表
A depends on B
B depends on C
D before C
E after C
Y 之后的X可以在X 之前轻易地重写为 Y
测试用例:
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]
奖励积分:算法也可以配置为强制排序中的第一个和最后一个节点。
答案 0 :(得分:2)
将整个图形的拓扑排序与感兴趣的节点的并集以及它们的依赖关系相交叉。在伪代码中:
λ N = A ∩ (N ∪ D)
其中 A 是拓扑排序图的有序集, N 是您关注的节点的子集, D 是 N 的依赖关系。请注意,交集运算符必须遵守 A 的顺序。
或者在Haskell中(使用节点数而不是字母,如示例所示):
import Data.List (intersect, union)
import Data.Graph (buildG, reachable, topSort)
graph = buildG (0, 4) [(3,2), (2,4), (2,1), (1,0)]
dependencies = buildG (0, 4) [(0, 1), (1, 2)]
ordering = topSort graph
f nodes = ordering `intersect` (nodes `union` deps)
where deps = concatMap (reachable dependencies) nodes
这假设您可以指定图表中的所有边。请注意,您只需要计算一次总排序,因此它应该在后续调用中执行。
上面的代码将输出:
> f [0]
[2,1,0]
> f [0, 3]
[3,2,1,0]
> f [1, 3, 4]
[3,2,4,1]
与您的上述测试用例匹配。
如果由于某种原因你无法指定图中的每条边,而只是指定相对约束,则按上述方法计算(N∪D)并应用约束满足。这样做的天真方法是尝试这些节点的每个排列,直到找到满足所有约束的节点。显然,你可以比使用简单的深度优先和更高效的方式更有效地做到这一点。回溯接近。
编辑:深度优先代码
非常简单。我们创建一个树,其中包含我们关心的节点的所有排列,然后 walk / prune那个树,直到我们找到满足所有约束的排列(请注意,我们将依赖关系附加到约束,因为依赖关系也是约束)。所有约束都以(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)减少测试约束的数量显然也会减少 isSatisfied 调用,占计算时间的25%左右。
如果要在严格执行的环境中实现这样的代码,则必须稍微修改代码结构。因此,这段代码在很大程度上依赖于排列树是懒惰的,这使得我们不必将搜索代码与树生成代码交织在一起,因为搜索代码根本不会沿着它认为不适合的路径走下去。
最后,如果您想要找到所有解决方案,而不是仅查找第一个解决方案,只需将搜索主体更改为:
| satisfies && null children = [path]
| satisfies = concatMap search children
| otherwise = []
我没有花时间优化这些代码或类似的东西,只是因为原始算法显然是优越的,假设你能够指定完整的图形(我相信,你可以)。