Эффективное решение проблемы одинаковой границы для бинарных деревьев

Граница бинарного дерева — это последовательность, состоящая из его листьев, от слева направо. Та же самая проблема [Hewitt & Patterson, 1970] состоит из определения того, имеют ли два двоичных дерева одинаковые края. Например, первые два дерева ниже имеют одинаковую бахрому, но последние два нет:

%         .         .         .
%        / \       / \       / \
%       .   3     1   .     1   .
%      / \           / \       / \
%     1   2         2   3    -2   3

example(1, fork(fork(leaf(1), leaf(2)), leaf(3))).
example(2, fork(leaf(1), fork(leaf(2), leaf(3)))).
example(3, fork(leaf(1), fork(leaf(-2), leaf(3)))).

Простое решение — собрать листья одного дерева в список и затем сравните их с листьями другого дерева.

/*
 * SIMPLE SOLUTION
 */

sf_1(T1, T2) :-
    walk(T1, [], Xs),
    walk(T2, [], Xs).

walk(leaf(X), A, [X|A]).
walk(fork(L, R), A0, Xs) :-
    walk(R, A0, A1),
    walk(L, A1, Xs).

Несмотря на простоту, это решение считается неэлегантным: во-первых, потому что это может оказаться непрактичным, если первое дерево очень большое; и, во-вторых, потому что это очень неэффективно, когда деревья различаются в первые несколько листья. Таким образом, лучшим решением было бы прекратить сравнение, как только при обнаружении первого различия без полного создания списка содержащий бахрому первого дерева.

/*
 * SUPPOSEDLY BETTER SOLUTION
 */

sf_2(T1, T2) :-
    step([T1], [T2]).

step([], []).
step([T1|S1], [T2|S2]) :-
    next(T1, S1, X, R1),
    next(T2, S2, X, R2),
    step(R1, R2).

next(leaf(X), S, X, S).
next(fork(L, R), S0, X, S) :-
    next(L, [R|S0], X, S).

Чтобы сравнить производительность этих двух решений, я реализовал несколько предикатов для проведения автоматических экспериментов (с помощью SWI-prolog версии 9.0.4):

/*
 * EMPIRICAL COMPARISON
 */

comp(Case) :-
    format('fsize sf-1 sf-2\n'),
    forall( between(1, 10, I),
            (   N is 100000 * I,
                tree(1, N, A),
                (   Case = true         % trees with same fringes
                ->  tree(1, N, B)
                ;   M is random(N//10), % trees with different fringes
                    flip(A, M, B) ),
                time(10, sf_1(A, B), T1),
                time(10, sf_2(A, B), T2),
                format('~0e ~2f ~2f\n', [N, T1, T2]) ) ).

time(N, G, T) :-
    garbage_collect,
    S is cputime,
    forall(between(1, N, _), ignore(call(G))),
    T is (cputime - S) / N.

/*
 * RANDOM TREE GENERATION AND MODIFICATION
 */

tree(X1, Xn, leaf(X1)) :-
    X1 = Xn,
    !.
tree(X1, Xn, fork(L, R)) :-
    X1 < Xn,
    random(X1, Xn, Xi),
    Xj is Xi + 1,
    tree(X1, Xi, L),
    tree(Xj, Xn, R).


flip(leaf(X), Y, leaf(Z)) :-
    (  X = Y
    -> Z is -X
    ;  Z is  X ).
flip(fork(L0, R0), X, fork(L, R)) :-
    flip(L0, X, L),
    flip(R0, X, R).

Эмпирические результаты показывают, что второе решение фактически быстрее первого, когда деревья не имеют одинаковых полос:

?- comp(false).
fsize sf-1 sf-2
1e+05 0.01 0.00
2e+05 0.03 0.00
3e+05 0.05 0.00
4e+05 0.07 0.01
5e+05 0.09 0.01
6e+05 0.11 0.00
7e+05 0.12 0.01
8e+05 0.14 0.01
9e+05 0.17 0.00
1e+06 0.18 0.00
true.

Однако если деревья имеют одинаковую бахрому, второе решение немного медленнее первого:

?- comp(true).
fsize sf-1 sf-2
1e+05 0.02 0.03
2e+05 0.04 0.05
3e+05 0.06 0.08
4e+05 0.08 0.11
5e+05 0.10 0.12
6e+05 0.12 0.14
7e+05 0.12 0.16
8e+05 0.14 0.18
9e+05 0.17 0.19
1e+06 0.18 0.22
true.

ВОПРОС: Возможно ли реализовать решение (на Прологе), которое будет быстрее, чем простое решение (на постоянный коэффициент, не обязательно асимптотически быстрее), когда полосы различны, но не медленнее, когда полосы одинаковы? Другими словами, можем ли мы добиться эффективного сравнения без накладных расходов? Если да, то как?

Если вы перенесете работу на функции модификации дерева, вы можете сохранить знаковый счетчик «unmatchedcount», скажем, листьев на дереве 1, а не на дереве 2, минус на дереве 2 и не на дереве 1 соответственно, тогда вы сможете узнать за время O (1), если они совпало или нет.

Simon Goater 15.06.2024 18:02

Предложение: используйте swi-prolog.org/pldoc/man?section=threadcom и выполняйте каждый обход в отдельном потоке (т. е. создайте 2 потока), при этом основной поток проверит, что листья, возвращенные с помощью thread_get_message, равны (хотя и в разные заказы). Приемлемо ли решение, специфичное для swi-prolog?

brebs 15.06.2024 21:30

Второй извлекает выгоду из некоторых микрооптимизаций. У него никогда не было лучшего большого О. Поиск микрооптимизаций для первого в случае, когда второй работает лучше всего, будет зависеть от языка, платформы и данных.

btilly 16.06.2024 01:18

@brebs Да, решение для swi-prolog было бы приемлемым. Спасибо за предложение.

slago 16.06.2024 02:18

@SimonGoater Создание/модификация дерева направлена ​​только на автоматическую генерацию данных для эмпирического сравнения предикатов sf_1/2 e sf_2/2. Два предложенных решения не должны создавать или изменять деревья, а лишь проверять, имеют ли они одинаковую полосу.

slago 16.06.2024 02:41

@btilly Точно! Очевидно, что оба решения в худшем случае равны O(n), поскольку невозможно сравнить все n листьев двух деревьев за меньшее время (когда я говорю «быстрее», я не имею в виду асимптотически быстрее), и фактически , желаемое решение должно быть специфичным для языка Пролог (именно поэтому вопрос помечен как пролог).

slago 16.06.2024 02:51

Интересный факт: бывают случаи, когда ускорение может составлять от O(N) до O(1)!

false 16.06.2024 15:10

Альтернатива тредам: продолжения с разделителями: swi-prolog.org/pldoc/man?section=delcont

brebs 17.06.2024 12:32

просто примечание: ваш sf_1 не «собирает листья одного дерева в список, а затем сравнивает их с листьями другого дерева». он собирает листья каждого дерева в свой список независимо, а затем, построив второй список до его головного элемента, сравнивает второй список с первым списком. это связано с тем, что порядок целей в walk -- A1 полностью независим от Xs. только когда будет достигнут самый левый лист, у нас будет { walk(L, A1, Xs) = walk(leaf(X), A, [X|A]) } === { L=leaf(X),A1=A,Xs=[X|A] } === { L=leaf(X), Xs=[X|A1] }.

Will Ness 19.06.2024 11:42

(продолжение) Это потому, что списки строятся снизу вверх. если бы списки строились сверху вниз, то второй walk, то есть обход по второму дереву с полностью построенным списком листьев из первого дерева, имел бы шанс потерпеть неудачу на первом несовпадающем листе из второго дерева. конечно, ему все равно придется сначала полностью пройти через первое дерево.

Will Ness 19.06.2024 11:53

Этот -2 кажется неоднозначным примером — демонстрирует ли он, что обход дерева не гарантирует упорядоченный список, или демонстрирует явно неправильное значение?

brebs 19.06.2024 17:45

Я не вижу, что двусмысленного в 1,2,3 против 1,-2,3.

Will Ness 19.06.2024 23:57

@false, вы приняли ответ, это более или менее то, что я имел в виду. Я не понимаю, что в этом плохого? Кстати, это «закапывание» в самый левый лист — та же идея, что и идея «суслика» Джона Маккарти, создателя LISP, за исключением того, что он на самом деле поворачивает дерево вправо, в то время как вы создаете стек, но это тоже самое.

Will Ness 24.06.2024 12:43

что с этим не так? Это могло бы быть намного лучше с точки зрения прекращения.

false 24.06.2024 13:40

Или стоимость может быть постоянной, если она равна O(max(N1,N2))

false 24.06.2024 13:42

Пример: ?- sf_(leaf(X),fork(_,_)). пока весь цикл, но это может привести к сбою. Таким образом, все остальные экземпляры представляют собой гигантские деревья с более чем одним fork.

false 24.06.2024 13:52

...кстати, даже ?- sf_(leaf(X),T)., очевидно, должно прекратиться.

false 24.06.2024 14:23

@false, поэтому теперь мы не видим вопрос, на который были опубликованы новые ответы, то есть требования к награде. Могу ли я предложить вместо этого в следующий раз опубликовать новый, отдельный вопрос, с обратной ссылкой на исходную запись (например, в данном случае), если это необходимо. Совершенно нормально назначить награду за собственный вопрос, чтобы «привлечь внимание к вопросу». таким образом сохраняются как исходный, так и последующие вопросы, и каждый ответ отображается под отдельным вопросом. Спасибо.

Will Ness 25.06.2024 15:58

@Will, согласен с отсутствующим текстом, который был примером вопроса ОП выше (то есть лучшее завершение, что означает лучшее O). Но снова публиковать новый вопрос при нынешнем низком трафике? Подумайте об этом, я намерен назначить еще одну награду за этот вопрос... Вопрос о награде тоже поставлю в качестве комментария.

false 25.06.2024 16:48

@false это действительно не общепринятый способ. подход SO предполагает, что ответы на вопрос отвечают на него, а не на что-то еще. то, как обстоят дела сейчас, действительно сбивает с толку. каждый раз, когда вы публикуете награду за существующий вопрос с новыми требованиями, создается эта проблема. трафик или его отсутствие — это ортогональный вопрос. Я думаю, если вы зададите вопрос, люди проявят интерес. если нет, вы бы добавили бонус, и они это заметят. комментарии должны быть эфемерными. это комментарии, а не содержимое.

Will Ness 25.06.2024 19:34

Все мои награды — это уточнения исходных вопросов. ОП просил не обязательно асимптотически быстрее.

false 25.06.2024 20:07

... однако, как вам кажется, @Will больше занимался SO-контролем, чем программированием.

false 26.06.2024 07:29

Я просто обратился к Вам со своим мнением. Вот для чего нужны комментарии.

Will Ness 27.06.2024 17:01
За пределами сигналов Angular: Сигналы и пользовательские стратегии рендеринга
За пределами сигналов Angular: Сигналы и пользовательские стратегии рендеринга
TL;DR: Angular Signals может облегчить отслеживание всех выражений в представлении (Component или EmbeddedView) и планирование пользовательских...
Sniper-CSS, избегайте неиспользуемых стилей
Sniper-CSS, избегайте неиспользуемых стилей
Это краткое руководство, в котором я хочу поделиться тем, как я перешел от 212 кБ CSS к 32,1 кБ (сокращение кода на 84,91%), по-прежнему используя...
5
23
558
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Ответ принят как подходящий

Объедините два подхода в один. Всегда лучше, чем sf_2. Пространственное значение должно быть больше или равно sf_1, поскольку первый список не генерируется. В SWI необходимо раскрыть цель next/3, чтобы всегда получать лучшее или равное время выполнения.

sf_3(T1, T2) :-
   stepping(T1, [T2],[]).

next(X, [T|Ts0],Ts) :-
   t_next(T, X, Ts0,Ts).

t_next(leaf(X), X, Ts,Ts).
t_next(fork(L, R), X, Ts0,Ts) :-
   t_next(L, X, [R|Ts0],Ts).

stepping(leaf(X), T2s0,T2s):-
   next(X, T2s0,T2s).
stepping(fork(L, R), T2s0,T2s) :-
   stepping(L, T2s0,T2s1),
   stepping(R, T2s1,T2s).

Удаление предиката next/3 и замена предложения stepping(leaf(X), T2s0, T2s) :- next(X, T2s0, T2s). на stepping(leaf(X), [T|Ts0], T2s ) :- t_next(T, X, Ts0, T2s). делает реализацию еще быстрее.

slago 17.06.2024 15:16

(Это то, что я имел в виду под разворачиванием next/3)

false 18.06.2024 09:35

Для справедливого сравнения исходная программа также должна быть написана в стиле Пролога. Информация об узлах в T1 уже может быть использована, чтобы ускорить сбой второго преобразования (в идеале мы могли бы потерпеть неудачу как можно скорее, но sf_1b просто дает сбой раньше). Лучше всего это можно наблюдать, глядя на завершение. Кроме того, эта версия позволяет избежать повторного создания этого списка для второго дерева.

sf_1b(T1, T2) :-
   phrase(leaves(T1), Xs),
   phrase(leaves(T2), Xs).

leaves(leaf(X)) -->
   [X].
leaves(fork(L,R)) -->
   leaves(L),
   leaves(R).

?- sf_1(leaf(1),fork(leaf(2),_)).
   loops, unexpected.

?- sf_1b(leaf(1),fork(leaf(2),_)).
   false. % as expected

Интересный! Предотвращая создание списка для второго дерева, предикат sf_1b/2 работает быстрее, чем sf_1/2, когда деревья не имеют одинаковых границ; однако, если у них одинаковая бахрома, sf_1b/2 может быть немного медленнее, чем sf_1/2.

slago 17.06.2024 14:59

@Абду: Вы удаляете ту самую информацию, которая представляет интерес для всех, кто понимает этот ответ. sf_1b не оптимальное решение.

false 18.06.2024 10:29

Для лучшего завершения:

tree_equal(leaf(L), leaf(L)).
tree_equal(fork(A1, A2), fork(B1, B2)) :-
    tree_equal(A1, B1),
    tree_equal(A2, B2).
% Two equivalent forking structures
tree_equal(fork(fork(A1, A2), A3), fork(B1, fork(B2, B3))) :-
    fork_equal(A1, A2, A3, B1, B2, B3).
tree_equal(fork(B1, fork(B2, B3)), fork(fork(A1, A2), A3)) :-
    fork_equal(A1, A2, A3, B1, B2, B3).

fork_equal(A1, A2, A3, B1, B2, B3) :-
    tree_equal(fork(A1, fork(A2, A3)), fork(B1, fork(B2, B3))).

Полученные результаты:

?- tree_equal(fork(fork(leaf(1), leaf(2)), leaf(3)),
   fork(leaf(1), fork(leaf(2), leaf(3)))).
true ;  % Succeeds
false.

?- tree_equal(fork(fork(leaf(1), leaf(2)), leaf(3)),
   fork(leaf(1), fork(leaf(-2), leaf(3)))).
false.

?- tree_equal(leaf(X),fork(_,_)).
false.  % Terminates

?- tree_equal(leaf(X),T).
T = leaf(X).  % Terminates

?- T1 = fork(leaf(1),fork(leaf(2),fork(leaf(3),leaf(4)))),
   T2 = fork(fork(leaf(1),fork(leaf(2),leaf(3))),leaf(4)),
   tree_equal(T1,T2).
T1 = fork(leaf(1), fork(leaf(2), fork(leaf(3), leaf(4)))),
T2 = fork(fork(leaf(1), fork(leaf(2), leaf(3))), leaf(4)) ;
false.
?- T1 = fork(leaf(1),fork(leaf(2),fork(leaf(3),leaf(4)))), T2 = fork(fork(leaf(1),fork(leaf(2),leaf(3))),leaf(4)), trees_equal(T1,T2). false, unexpected.
false 24.06.2024 17:33

(Это поможет придерживаться соглашения об именах. trees предлагает список tree)

false 24.06.2024 17:34

Зафиксированный. Я надеюсь (не уверен), что теперь это достаточно общее.

brebs 24.06.2024 18:55

(кажется, правильно) ?- T1 = fork(leaf(1),fork(leaf(2),leaf(3))), tree_equal(T1,T2), false. loops. это не требовалось для награды, если вы найдете улучшение, используйте другой ответ.

false 24.06.2024 19:21

Именно это определение я имел в виду. Он завершается, если указан первый аргумент. Но сначала, почему sf_1b не идеально использовать срез отказа

leaves(leaf(X)) --> {false},
   [X].
leaves(fork(L,R)) -->
   leaves(L), {false},
   leaves(R).

?- phrase(leaves(T), []).
   loops, unexpected.
   false. % expected, but not found.

Итак, пример левой рекурсии, которой можно легко избежать.

sf_1c(T1, T2) :-
   phrase(leaves(T1), Xs),
   tree_leaves(T2, Xs).

tree_leaves(T, Xs) :-
   Xs = [_|C],
   phrase(tleaves(T, C,[]), Xs).

tleaves(leaf(X), C,C) -->
   [X].
tleaves(fork(L,R), [_|C0],C) -->
   tleaves(L, C0,C1),
   tleaves(R, C1,C).

Теперь это завершается для первого аргумента:

sf_1c(A,B)terminates_if b(A).
    % optimal. loops found: [sf_1c(fork(_,_),_),sf_1c(fork(_,_),y)].

Вследствие этого свойства завершения сложность основных запросов (в худшем случае) зависит (в данном случае) исключительно от аргументов, упомянутых в условии завершения. Остальные аргументы, в данном случае второй, не имеют никакого влияния! В лучшем случае они могут улучшить сложность, но никогда не смогут ее ухудшить. Лучше всего это можно наблюдать, глядя на время выполнения левых деревьев, которые являются линейными, а не постоянными для решений до sf_1c.

leftleaves(leaf(N)) -->
   [N].
leftleaves(fork(T,leaf(N))) -->
   [N],
   leftleaves(T).

?- T1 = leaf(1), numbered_from(L, 2), ( true ; phrase(leftleaves(T2),L), time(sf_1c(T1,T2)) ).

Это еще можно улучшить, возможно, за еще одну награду...

Для правильного завершения и улучшения моего предыдущего ответа:

sf_br10(leaf(L), leaf(L)).
sf_br10(fork(A1, A2), fork(B1, B2)) :-
    sf_br10_leaf(A1, B1, [A1, A2], [B1, B2], A2, B2, AR, BR),
    sf_br10(AR, BR).

% Compare left-most leaves
sf_br10_leaf(leaf(L), leaf(L), _, _, A, B, A, B).
sf_br10_leaf(leaf(L), fork(B1, B2), AF, BF, AU, BU, AR, BR) :-
    sf_br10_fork(AF, AFR),
    sf_br10_leaf(leaf(L), B1, AFR, BF, AU, fork(B2, BU), AR, BR).
sf_br10_leaf(fork(A1, A2), leaf(L), AF, BF, AU, BU, AR, BR) :-
    sf_br10_fork(BF, BFR),
    sf_br10_leaf(A1, leaf(L), AF, BFR, fork(A2, AU), BU, AR, BR).
sf_br10_leaf(fork(A1, A2), fork(B1, B2), AF, BF, AU, BU, AR, BR) :-
    sf_br10_fork(AF, AFR),
    sf_br10_fork(BF, BFR),
    sf_br10_leaf(A1, B1, AFR, BFR, fork(A2, AU), fork(B2, BU), AR, BR).

% Ensures a fork on both sides, for termination
sf_br10_fork([fork(L, R)|T], [L,R|T]).
sf_br10_fork([leaf(_)|T], R) :-
    sf_br10_fork(T, R).

Полученные результаты:

?- sf_br10(fork(fork(leaf(1), leaf(2)), leaf(3)),
   fork(leaf(1), fork(leaf(2), leaf(3)))).
true ;  % Succeeds
false.

?- sf_br10(fork(fork(leaf(1), leaf(2)), leaf(3)),
   fork(leaf(1), fork(leaf(-2), leaf(3)))).
false.  % As intended

?- sf_br10(leaf(X), fork(_,_)).
false.  % Terminates

?- sf_br10(leaf(X), T).
T = leaf(X).  % Terminates

?- T1 = fork(leaf(1),fork(leaf(2),leaf(3))), sf_br10(T2,T1).
T1 = T2, T2 = fork(leaf(1), fork(leaf(2), leaf(3))) ;
T1 = fork(leaf(1), fork(leaf(2), leaf(3))),
T2 = fork(fork(leaf(1), leaf(2)), leaf(3)) ;
false.  % Terminates

?- T = fork(leaf(1),fork(leaf(2),fork(leaf(3),leaf(4)))), sf_br10(T,T2).
T = T2, T2 = fork(leaf(1), fork(leaf(2), fork(leaf(3), leaf(4)))) ;
T = fork(leaf(1), fork(leaf(2), fork(leaf(3), leaf(4)))),
T2 = fork(leaf(1), fork(fork(leaf(2), leaf(3)), leaf(4))) ;
T = fork(leaf(1), fork(leaf(2), fork(leaf(3), leaf(4)))),
T2 = fork(fork(leaf(1), leaf(2)), fork(leaf(3), leaf(4))) ;
T = fork(leaf(1), fork(leaf(2), fork(leaf(3), leaf(4)))),
T2 = fork(fork(leaf(1), fork(leaf(2), leaf(3))), leaf(4)) ;
T = fork(leaf(1), fork(leaf(2), fork(leaf(3), leaf(4)))),
T2 = fork(fork(fork(leaf(1), leaf(2)), leaf(3)), leaf(4)) ;
false.  % Terminates
?- T = fork(leaf(1),fork(leaf(2),fork(leaf(3),leaf(4)))), tree_equal(T,_), false. loops, unexpected. Пожалуйста, проведите тестирование перед публикацией.
false 25.06.2024 17:08

Вот тест: ?- numbered_from(L,1),tree_leaves(T,L), ( true ; tree_equal(T,_), false ). выполнит цикл для поиска простейшего контрпримера, если он есть.

false 25.06.2024 17:09

Также обратите внимание, что ваш member_exists/2 по-прежнему выдает несколько ответов из-за переменных _. ?- member_exists(fork(_,_),[fork(leaf(1),leaf(2)),fork(leaf(3),‌​leaf(4))]). true ; true ; false.

false 25.06.2024 21:09

Приношу свои извинения, исправлено с использованием простого метода, который я ранее не учитывал. Подскажите, пожалуйста, где код numbered_from?

brebs 25.06.2024 21:22

Найдите его в SO или в Google :-)

false 26.06.2024 05:36

И еще: было бы полезно, если бы вы придерживались оригинальных названий, примерно sf_br99 или около того, чтобы все версии можно было загружать одновременно без какого-либо дополнительного модуляции.

false 26.06.2024 05:52

Прекращение в порядке, но теперь оно неполное. ?- T = fork(leaf(1),fork(leaf(2),fork(leaf(3),leaf(4)))), T2 = fork(fork(leaf(1),fork(leaf(2),leaf(3))),leaf(4)), tree_equal(T,T2). false, unexpected.

false 26.06.2024 06:08
?- T = fork(leaf(1),fork(leaf(2),_)), T2 = fork(fork(_,fork(_,_)),_), tree_equal(T,T2). false, unexpected. Так что даже это обобщение неверно.
false 26.06.2024 09:15

Наконец-то достигнута полнота и завершение.

brebs 28.06.2024 09:49

Другие вопросы по теме