【问题标题】:Dynamic union find algorithm in PrologProlog中的动态联合查找算法
【发布时间】:2020-11-12 00:35:16
【问题描述】:

假设我有集合 S1,..,Sn 并且我们想要找到 最小覆盖 C1,..,Cm 使得在每个覆盖中永远不会不相交 连接组件。

例如集合 S1=[X,Y], S2=[Y,Z], S3=[T] I 会找到封面 C1=[X,Y,Z] 和 C2=[T]。关于什么 可以动态分割封面的动态算法?

假设元素 Y 死亡,那么我们就剩下 S1'=[X]、S2'=[Z] 和 S3'=[T]。封面现在 C1'=[X]、C2'=[Z] 和 C3'=[T]。所以封面的数量 增加了。

联合查找算法可以确定给定的覆盖 集合的集合,但我担心每当元素死亡时重新计算完整集合,效率不高。

【问题讨论】:

  • 一个元素是如何“死亡”的?
  • 通过一些“死亡”事件。例如,在 SAT 求解器的实践中,元素可能是一个变量,然后实例化为 0 或 1,并且不再参与联合查找算法。

标签: prolog union-find


【解决方案1】:

为了挖掘 Prolog 系统的资源,我做了一个基于 copy_term/2 和 keysort/2 的小联合查找算法。算法here的主要入口点做了以下工作:

covers(L, S) :-
   vars_list(L, K),
   copy_term(K, R),
   make_keys(L, R, H),
   keysort(H, J),
   collect_keys(J, S).

这是一个运行示例:

?- covers([X+Y,Y+Z,T], C).
C = [[X+Y, Y+Z], [T]]

要获得动态算法,我们可以尝试以下方法。保持一个可回溯的结构,允许从元素中找到封面。那么如果一个元素死亡,只重新计算属于死亡元素的封面。

这会稍微降低复杂性。否则我在这里没有更好的主意,除了观察到死亡元素只会将自己的封面分成更小的封面。

【讨论】:

【解决方案2】:

我认为这有两个困难的部分:

  • 一种非常高效的地图数据结构
  • 一种高效的数据结构,用于跟踪要重新计算的内容

联合查找数据结构本身已经在 Prolog 中实现,但隐藏在“变量”这个有点晦涩的名称后面。如果您有办法将术语与表示它们的并集等价类的变量相关联,那么:

  • find 操作是查找类变量
  • 测试两个类是否相同是Class1 == Class2
  • union 操作是Class1 = Class2

所以find 是一个瓶颈。在命令式语言中,查找通常是常数时间,使用类似foo.equivalence_classequivalence_classes[foo.id] 的东西。通常,在 Prolog 中,我们没有等效的(近)恒定时间映射。但由于您似乎只对变量感兴趣,SWI-Prolog 的 attributed variables 确实符合要求!

我们可以为这样的术语列表计算联合查找等价类:

compute_classes([]).
compute_classes([Term | Terms]) :-
    term_variables(Term, Variables),
    variables_class(Variables, _NewEquivalenceClass),
    compute_classes(Terms).

variables_class([], _EquivalenceClass).
variables_class([Var | Vars], EquivalenceClass) :-
    (   get_attr(Var, equivalence_class, ExistingEquivalenceClass)
    ->  ExistingEquivalenceClass = EquivalenceClass
    ;   put_attr(Var, equivalence_class, EquivalenceClass) ),
    variables_class(Vars, EquivalenceClass).

使用您的示例:

?- compute_classes([X+Y, Y+Z, T]).
put_attr(X, equivalence_class, _2772),
put_attr(Y, equivalence_class, _2772),
put_attr(Z, equivalence_class, _2772),
put_attr(T, equivalence_class, _2814).

我们可以看到XYZ 都共享一个等价类,而T 在一个单独的类中。

一些实用程序:

var_class(Var, Class) :-
    get_attr(Var, equivalence_class, Class).

var_var_sameclass(Var1, Var2) :-
    var_class(Var1, Class1),
    var_class(Var2, Class2),
    Class1 == Class2.

var_var_union(Var1, Var2) :-
    var_class(Var1, Class1),
    var_class(Var2, Class2),
    Class1 = Class2.

继续举例:

?- compute_classes([X+Y, Y+Z, T]), var_class(X, ClassX), var_class(Y, ClassY), var_class(T, ClassT).
ClassX = ClassY,
put_attr(X, equivalence_class, ClassY),
put_attr(Y, equivalence_class, ClassY),
put_attr(Z, equivalence_class, ClassY),
put_attr(T, equivalence_class, ClassT).

?- compute_classes([X+Y, Y+Z, T]), var_var_sameclass(X, Y).
put_attr(X, equivalence_class, _3436),
put_attr(Y, equivalence_class, _3436),
put_attr(Z, equivalence_class, _3436),
put_attr(T, equivalence_class, _3478).

?- compute_classes([X+Y, Y+Z, T]), var_var_sameclass(X, T).
false.

?- compute_classes([X+Y, Y+Z, T]), var_var_union(Z, T), var_var_sameclass(X, T). 
put_attr(X, equivalence_class, _3502),
put_attr(Y, equivalence_class, _3502),
put_attr(Z, equivalence_class, _3502),
put_attr(T, equivalence_class, _3502).

也就是说,XY 确实属于同一类,而 XT 不是。如果我们合并ZT 的类,那么XT 会突然在同一个类中。

杀死变量是更乏味的地方。这里的想法(正如您在问题中所建议的那样)是仅重新计算输入的“受影响”部分。我认为这可以通过将一组受影响的术语与每个等价类相关联来完成。我将在这里使用列表,但我不建议在实践中使用列表是一个好的选择。

计算术语列表的等价类,以及每个等价类的“监视列表”:

compute_classes_and_watchlists(Terms) :-
    compute_classes(Terms),
    maplist(compute_watchlist, Terms).

compute_watchlist(Term) :-
    term_variables(Term, [RepresentativeVariable | _OtherVars]),
    var_class(RepresentativeVariable, Class),
    (   get_attr(Class, class_watchlist, Watchlist)
    ->  true
    ;   Watchlist = [] ),
    put_attr(Class, class_watchlist, [Term | Watchlist]).

例如:

?- compute_classes_and_watchlists([X+Y, Y+Z, T]).
put_attr(X, equivalence_class, _2932),
put_attr(_2932, class_watchlist, [Y+Z, X+Y]),
put_attr(Y, equivalence_class, _2932),
put_attr(Z, equivalence_class, _2932),
put_attr(T, equivalence_class, _3012),
put_attr(_3012, class_watchlist, [T]).

因此,如果您要杀死 XYZ 中的任何一个,他们的类 _2932 的监视列表会告诉您,您需要重新计算术语 Y+ZX+Y(但没有别的)。

Killing 本身获取被杀死变量的类及其监视列表(它“返回”)并清除该类中每个变量的等价类:

kill_var(Var, TermsToRecompute) :-
    var_class(Var, Class),
    get_attr(Class, class_watchlist, TermsToRecompute),
    del_attr(Class, class_watchlist),
    maplist(clear_class, TermsToRecompute).

clear_class(Term) :-
    term_variables(Term, [RepresentativeVariable | _OtherVars]),
    del_attr(RepresentativeVariable, equivalence_class).

只有在您立即 (a) 将被杀死的变量绑定到基本术语,并且 (b) 重新计算受影响术语的等价类时,才有意义。在您的示例中:

?- compute_classes_and_watchlists([X+Y, Y+Z, T]), kill_var(Y, TermsToRecompute), Y = y_is_now_bound, compute_classes_and_watchlists(TermsToRecompute).
Y = y_is_now_bound,
TermsToRecompute = [y_is_now_bound+Z, X+y_is_now_bound],
put_attr(X, equivalence_class, _4640),
put_attr(_4640, class_watchlist, [X+y_is_now_bound]),
put_attr(Z, equivalence_class, _4674),
put_attr(_4674, class_watchlist, [y_is_now_bound+Z]),
put_attr(T, equivalence_class, _4708),
put_attr(_4708, class_watchlist, [T]).

这变得难以阅读,但关键是在杀死和绑定 Y 并在受影响的术语上重新计算 union-find 结构后,XZ 现在位于不同的等价类中。 T 只是坐着不受影响。

所有这些都假设您不会在联合中回溯,尽管我对 SWI 的属性化 var 文档的阅读是,这甚至不会做任何非常错误的事情。由于put_attr 是可回溯的,因此需要更加小心,这可能会成为一个相当灵活的实现,其中回溯只是将类再次拆分。我认为,也可以让杀戮中的回溯也起作用。

待办事项:

  • 在监视列表的情况下,必须更改var_var_union 的定义以合并两个类的监视列表(如果它们不同);对于实际列表,这将是append,但是一些实际的集合或更专业的数据结构会更好,特别是如果您希望有某种“类似堆栈”的行为,其中下一个要被杀死的变量很可能是你最近做了一次union 操作
  • 尤其是监视列表的方法,必须防止用户意外地统一equivalence_class术语;这可以通过使用某种 class(<unique_id>, NakedClassVariable) 而不是仅仅使用裸变量来表示等价类来完成
  • term_variables 的重复调用可能会以某种方式进行优化 - 除了术语监视列表之外,您还可以保留感兴趣变量的监视列表

总而言之,这不是完全适合生产的代码,但它可能会给你一些想法。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-10-23
    • 1970-01-01
    • 2021-10-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多