Граница бинарного дерева — это последовательность, состоящая из его листьев, от слева направо. Та же самая проблема [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.
ВОПРОС: Возможно ли реализовать решение (на Прологе), которое будет быстрее, чем простое решение (на постоянный коэффициент, не обязательно асимптотически быстрее), когда полосы различны, но не медленнее, когда полосы одинаковы? Другими словами, можем ли мы добиться эффективного сравнения без накладных расходов? Если да, то как?
Предложение: используйте swi-prolog.org/pldoc/man?section=threadcom и выполняйте каждый обход в отдельном потоке (т. е. создайте 2 потока), при этом основной поток проверит, что листья, возвращенные с помощью thread_get_message
, равны (хотя и в разные заказы). Приемлемо ли решение, специфичное для swi-prolog?
Второй извлекает выгоду из некоторых микрооптимизаций. У него никогда не было лучшего большого О. Поиск микрооптимизаций для первого в случае, когда второй работает лучше всего, будет зависеть от языка, платформы и данных.
@brebs Да, решение для swi-prolog было бы приемлемым. Спасибо за предложение.
@SimonGoater Создание/модификация дерева направлена только на автоматическую генерацию данных для эмпирического сравнения предикатов sf_1/2
e sf_2/2
. Два предложенных решения не должны создавать или изменять деревья, а лишь проверять, имеют ли они одинаковую полосу.
@btilly Точно! Очевидно, что оба решения в худшем случае равны O(n), поскольку невозможно сравнить все n листьев двух деревьев за меньшее время (когда я говорю «быстрее», я не имею в виду асимптотически быстрее), и фактически , желаемое решение должно быть специфичным для языка Пролог (именно поэтому вопрос помечен как пролог).
Интересный факт: бывают случаи, когда ускорение может составлять от O(N) до O(1)!
Альтернатива тредам: продолжения с разделителями: swi-prolog.org/pldoc/man?section=delcont
просто примечание: ваш 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] }
.
(продолжение) Это потому, что списки строятся снизу вверх. если бы списки строились сверху вниз, то второй walk
, то есть обход по второму дереву с полностью построенным списком листьев из первого дерева, имел бы шанс потерпеть неудачу на первом несовпадающем листе из второго дерева. конечно, ему все равно придется сначала полностью пройти через первое дерево.
Этот -2
кажется неоднозначным примером — демонстрирует ли он, что обход дерева не гарантирует упорядоченный список, или демонстрирует явно неправильное значение?
Я не вижу, что двусмысленного в 1,2,3 против 1,-2,3.
@false, вы приняли ответ, это более или менее то, что я имел в виду. Я не понимаю, что в этом плохого? Кстати, это «закапывание» в самый левый лист — та же идея, что и идея «суслика» Джона Маккарти, создателя LISP, за исключением того, что он на самом деле поворачивает дерево вправо, в то время как вы создаете стек, но это тоже самое.
что с этим не так? Это могло бы быть намного лучше с точки зрения прекращения.
Или стоимость может быть постоянной, если она равна O(max(N1,N2))
Пример: ?- sf_(leaf(X),fork(_,_)).
пока весь цикл, но это может привести к сбою. Таким образом, все остальные экземпляры представляют собой гигантские деревья с более чем одним fork
.
...кстати, даже ?- sf_(leaf(X),T).
, очевидно, должно прекратиться.
@false, поэтому теперь мы не видим вопрос, на который были опубликованы новые ответы, то есть требования к награде. Могу ли я предложить вместо этого в следующий раз опубликовать новый, отдельный вопрос, с обратной ссылкой на исходную запись (например, в данном случае), если это необходимо. Совершенно нормально назначить награду за собственный вопрос, чтобы «привлечь внимание к вопросу». таким образом сохраняются как исходный, так и последующие вопросы, и каждый ответ отображается под отдельным вопросом. Спасибо.
@Will, согласен с отсутствующим текстом, который был примером вопроса ОП выше (то есть лучшее завершение, что означает лучшее O). Но снова публиковать новый вопрос при нынешнем низком трафике? Подумайте об этом, я намерен назначить еще одну награду за этот вопрос... Вопрос о награде тоже поставлю в качестве комментария.
@false это действительно не общепринятый способ. подход SO предполагает, что ответы на вопрос отвечают на него, а не на что-то еще. то, как обстоят дела сейчас, действительно сбивает с толку. каждый раз, когда вы публикуете награду за существующий вопрос с новыми требованиями, создается эта проблема. трафик или его отсутствие — это ортогональный вопрос. Я думаю, если вы зададите вопрос, люди проявят интерес. если нет, вы бы добавили бонус, и они это заметят. комментарии должны быть эфемерными. это комментарии, а не содержимое.
Все мои награды — это уточнения исходных вопросов. ОП просил не обязательно асимптотически быстрее.
... однако, как вам кажется, @Will больше занимался SO-контролем, чем программированием.
Я просто обратился к Вам со своим мнением. Вот для чего нужны комментарии.
Объедините два подхода в один. Всегда лучше, чем 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).
делает реализацию еще быстрее.
(Это то, что я имел в виду под разворачиванием next/3
)
Для справедливого сравнения исходная программа также должна быть написана в стиле Пролога. Информация об узлах в 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
.
@Абду: Вы удаляете ту самую информацию, которая представляет интерес для всех, кто понимает этот ответ. sf_1b
не оптимальное решение.
Для лучшего завершения:
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.
(Это поможет придерживаться соглашения об именах. trees
предлагает список tree
)
Зафиксированный. Я надеюсь (не уверен), что теперь это достаточно общее.
(кажется, правильно) ?- T1 = fork(leaf(1),fork(leaf(2),leaf(3))), tree_equal(T1,T2), false. loops.
это не требовалось для награды, если вы найдете улучшение, используйте другой ответ.
Именно это определение я имел в виду. Он завершается, если указан первый аргумент. Но сначала, почему 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.
Пожалуйста, проведите тестирование перед публикацией.
Вот тест: ?- numbered_from(L,1),tree_leaves(T,L), ( true ; tree_equal(T,_), false ).
выполнит цикл для поиска простейшего контрпримера, если он есть.
Также обратите внимание, что ваш member_exists/2
по-прежнему выдает несколько ответов из-за переменных _. ?- member_exists(fork(_,_),[fork(leaf(1),leaf(2)),fork(leaf(3),leaf(4))]). true ; true ; false.
Приношу свои извинения, исправлено с использованием простого метода, который я ранее не учитывал. Подскажите, пожалуйста, где код numbered_from
?
Найдите его в SO или в Google :-)
И еще: было бы полезно, если бы вы придерживались оригинальных названий, примерно sf_br99
или около того, чтобы все версии можно было загружать одновременно без какого-либо дополнительного модуляции.
Прекращение в порядке, но теперь оно неполное. ?- 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.
?- T = fork(leaf(1),fork(leaf(2),_)), T2 = fork(fork(_,fork(_,_)),_), tree_equal(T,T2). false, unexpected.
Так что даже это обобщение неверно.
Наконец-то достигнута полнота и завершение.
Если вы перенесете работу на функции модификации дерева, вы можете сохранить знаковый счетчик «unmatchedcount», скажем, листьев на дереве 1, а не на дереве 2, минус на дереве 2 и не на дереве 1 соответственно, тогда вы сможете узнать за время O (1), если они совпало или нет.