2012-06-07 2 views
7

Я уже сделал рабочий генератор verbal arithmetic в Prolog, но он слишком медленный. Требуется 8 минут, чтобы запустить простое выражение S E N D + M O R E = M O N E Y. Может ли кто-нибудь помочь мне ускорить его работу?Более быстрая реализация словесной арифметики в Prolog

/* verbalArithmetic(List,Word1,Word2,Word3) where List is the list of all 
possible letters in the words. The SEND+MORE = MONEY expression would then 
be represented as 
    verbalArithmetic([S,E,N,D,M,O,R,Y],[S,E,N,D],[M,O,R,E],[M,O,N,E,Y]). */ 

validDigit(X) :- member(X,[0,1,2,3,4,5,6,7,8,9]). 
validStart(X) :- member(X,[1,2,3,4,5,6,7,8,9]). 
assign([H|[]]) :- validDigit(H).   
assign([H|Tail]) :- validDigit(H), assign(Tail), fd_all_different([H|Tail]). 

findTail(List,H,T) :- append(H,[T],List). 

convert([T],T) :- validDigit(T). 
convert(List,Num) :- findTail(List,H,T), convert(H,HDigit), Num is (HDigit*10+T). 

verbalArithmetic(WordList,[H1|Tail1],[H2|Tail2],Word3) :- 
    validStart(H1), validStart(H2), assign(WordList), 
    convert([H1|Tail1],Num1),convert([H2|Tail2],Num2), convert(Word3,Num3), 
    Sum is Num1+Num2, Num3 = Sum. 

ответ

3

Примечание: В этом ответе обсуждается алгоритм сокращения количества комбинаций, которые необходимо попробовать. Я не знаю Prolog, поэтому я не могу предоставить фрагменты кода.

Уловка для ускорения решения грубой силы - это ярлыки. Если вы можете определить диапазон комбинаций, которые являются недействительными, вы можете существенно уменьшить количество комбинаций.

Посмотрите на пример. Когда человек решает это, она сразу же замечает, что ДЕНЬГИ имеет 5 цифр, а SEND и MORE - только 4, поэтому M в MONEY должна быть цифрой 1. 90% комбинаций ушли!

При построении алгоритма для компьютера мы пытаемся использовать ярлыки, которые применяются ко всем возможным вводам. Если они не дают требуемой производительности, мы начинаем искать ярлыки, которые применяются только к конкретным комбинациям ввода. Итак, теперь мы оставляем ярлык M = 1.

Вместо этого я бы сосредоточился на последних цифрах. Мы знаем, что (D + E) mod 10 = Y. Это наше 90% -ное сокращение количества комбинаций, которые нужно попробовать.

Этот шаг должен довести до конца менее минуты.

Что мы можем сделать, если этого недостаточно? Следующий шаг: Посмотрите на цифру от последнего до последнего! Мы знаем, что (N + R + несут из D + E) mod 10 = E.

Поскольку мы тестируем все действующие комбинации последней цифры, для каждого теста мы будем знать, является ли перенос 0 или 1 . Усложнение (для кода), которое дополнительно уменьшает количество тестируемых комбинаций, заключается в том, что мы будем сталкиваться с дубликатами (письмо сопоставляется с числом, которое уже назначено другой букве). Когда мы сталкиваемся с дубликатом, мы можем перейти к следующей комбинации, не двигаясь дальше по цепочке.

Удачи вам в назначении!

+1

Очень хорошие рассуждения, +1! Это именно то, что версия CLP (FD) делает для вас за кулисами. Например, когда я запрашиваю: '? - головоломка ([S, E, N, D] + [M, O, R, E] = [M, O, N, E, Y])., Тогда я получаю как переменные привязки: «M = 1, O = 0, S = 9', поэтому 3 переменные легко фиксируются на конкретные целые числа, просто отправив ограничения CLP (FD), которые описывают головоломку. Домены остальных переменных также уменьшаются, как видно из остаточных целей: «N в 5..8, E в 4..7, R в 2..8, Y в 2..8'. Конечный шаг поиска находит уникальное решение как конкретные целые привязки для всех переменных CLP (FD). – mat

6

Рассмотрите возможность использования finite domain constraints, например, в SWI-Пролог:

:- use_module(library(clpfd)). 

puzzle([S,E,N,D] + [M,O,R,E] = [M,O,N,E,Y]) :- 
     Vars = [S,E,N,D,M,O,R,Y], 
     Vars ins 0..9, 
     all_different(Vars), 
        S*1000 + E*100 + N*10 + D + 
        M*1000 + O*100 + R*10 + E #= 
     M*10000 + O*1000 + N*100 + E*10 + Y, 
     M #\= 0, S #\= 0. 

Пример запроса:

?- time((puzzle(As+Bs=Cs), label(As))). 
% 5,803 inferences, 0.002 CPU in 0.002 seconds (98% CPU, 3553582 Lips) 
As = [9, 5, 6, 7], 
Bs = [1, 0, 8, 5], 
Cs = [1, 0, 6, 5, 2] ; 
% 1,411 inferences, 0.001 CPU in 0.001 seconds (97% CPU, 2093472 Lips) 
false. 
4

Низкая производительность здесь происходит из-за формирования всех возможных назначения букв перед проверкой если осуществимы ,

Мой совет: «Сбой рано, не часто». То есть, нажимайте столько проверок на отказ как можно раньше на шаги назначения, таким образом обрезая дерево поиска.

Klas Lindbäck предлагает хорошие предложения. В качестве обобщения при добавлении двух чисел перенос не более одного в каждом месте. Таким образом, назначение отдельных цифр буквам слева направо может быть проверено с учетом возможности пока еще неопределенного переноса в самых правых местах. (Разумеется, в финальных «единицах» места нет.)

Это много, о чем нужно думать, поэтому логика ограничений, как подсказывает мата (и которую вы уже обсуждали с fd_all_different/1), является таким удобством.


Добавлено: Вот решение Пролог без ограничений логики, используя только один вспомогательный предикат, опускаем/3:

omit(H,[H|T],T). 
omit(X,[H|T],[H|Y]) :- omit(X,T,Y). 

которые оба выбирает элемент из списка и производит сокращенный список без этого элемента.

Вот тогда код для sendMoreMoney/3, который ищет, вычислив сумму слева направо:

sendMoreMoney([S,E,N,D],[M,O,R,E],[M,O,N,E,Y]) :- 
    M = 1, 
    omit(S,[2,3,4,5,6,7,8,9],PoolO), 
    (CarryS = 0 ; CarryS = 1), 
    %% CarryS + S + M =  M*10 + O 
    O is (CarryS + S + M) - (M*10), 
    omit(O,[0|PoolO],PoolE), 
    omit(E,PoolE,PoolN), 
    (CarryE = 0 ; CarryE = 1), 
    %% CarryE + E + O = CarryS*10 + N 
    N is (CarryE + E + O) - (CarryS*10), 
    omit(N,PoolN,PoolR), 
    (CarryN = 0 ; CarryN = 1), 
    %% CarryN + N + R = CarryE*10 + E 
    R is (CarryE*10 + E) - (CarryN + N), 
    omit(R,PoolR,PoolD), 
    omit(D,PoolD,PoolY), 
    %%   D + E = CarryN*10 + Y 
    Y is (D + E) - (CarryN*10), 
    omit(Y,PoolY,_). 

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


Добавлено (2): Вот «общий» cryptarithm решатель для двух слагаемых, которые не должны иметь одинаковую длину/количество «мест». Код для длины/2 опущен как довольно общий встроенного предикат, и, взяв предложение по завещанию Несс, призывает опускает/3 заменяются выбрать/3 для удобства пользователей SWI-Prolog.

Я проверил это с Amzi! и SWI-Prolog с использованием примеров альфа-метрики from Cryptarithms.com, которые включают в себя два слагаемых, каждый из которых имеет уникальное решение. Я также составил пример с десятком решений, I + AM = BEN, чтобы проверить правильное обратное отслеживание.

solveCryptarithm([H1|T1],[H2|T2],Sum) :- 
    operandAlign([H1|T1],[H2|T2],Sum,AddTop,AddPad,Carry,TSum,Pool), 
    solveCryptarithmAux(H1,H2,AddTop,AddPad,Carry,TSum,Pool). 

operandAlign(Add1,Add2,Sum,AddTop,AddPad,Carry,TSum,Pool) :- 
    operandSwapPad(Add1,Add2,Length,AddTop,AddPad), 
    length(Sum,Size), 
    ( Size = Length 
    -> (Carry = 0, Sum = TSum , Pool = [1|Peel]) 
    ; (Size is Length+1, Carry = 1, Sum = [Carry|TSum], Pool = Peel) 
    ), 
    Peel = [2,3,4,5,6,7,8,9,0]. 

operandSwapPad(List1,List2,Length,Longer,Padded) :- 
    length(List1,Length1), 
    length(List2,Length2), 
    ( Length1 >= Length2 
    -> (Length = Length1, Longer = List1, Shorter = List2, Pad is Length1 - Length2) 
    ; (Length = Length2, Longer = List2, Shorter = List1, Pad is Length2 - Length1) 
    ), 
    zeroPad(Shorter,Pad,Padded). 

zeroPad(L,0,L). 
zeroPad(L,K,P) :- 
    K > 0, 
    M is K-1, 
    zeroPad([0|L],M,P). 

solveCryptarithmAux(_,_,[],[],0,[],_). 
solveCryptarithmAux(NZ1,NZ2,[H1|T1],[H2|T2],CarryOut,[H3|T3],Pool) :- 
    (CarryIn = 0 ; CarryIn = 1), /* anticipatory carry */ 
    ( var(H1) 
    -> select(H1,Pool,P_ol) 
    ; Pool = P_ol 
    ), 
    ( var(H2) 
    -> select(H2,P_ol,P__l) 
    ; P_ol = P__l 
    ), 
    ( var(H3) 
    -> (H3 is H1 + H2 + CarryIn - 10*CarryOut, select(H3,P__l,P___)) 
    ; (H3 is H1 + H2 + CarryIn - 10*CarryOut, P__l = P___) 
    ), 
    NZ1 \== 0, 
    NZ2 \== 0, 
    solveCryptarithmAux(NZ1,NZ2,T1,T2,CarryIn,T3,P___). 

Я думаю, что это показывает, что преимущества слева направо поиск/оценка может быть достигнута в «обобщенной» решателя, увеличение числа выводов примерно в два раза по сравнению с ранее " индивидуальный "код.

+0

ваш 'omit/3' - это SWI-Prolog [' select/3'] (http://www.swi-prolog.org/pldoc/doc_for?object=select/3). Разнообразно известен как 'del/3',' delete/3' и т. Д. Использование этого позволяет напрямую манипулировать конечными доменами (или «пулами»). Предикат 'selectM/3' из моего ответа содержит несколько вызовов' select/3' в один, для более легкого и гораздо более короткого кодирования. Кроме того, ваш код использует много человеческих рассуждений. –

+0

@WillNess: Это правда, что SWI-Prolog имеет этот (эквивалентный) предикат как встроенный. Я пытался проиллюстрировать преимущество оценки слева направо, которая благодаря вашей версии справа налево позволяет сравнить. – hardmath

+0

Итак, я попробовал вашу версию, и потребовалось 533 (676) выводов/0.00 сек. Против 27 653 (38 601) выводов/0,02 сек, которые моя версия занимает. :) Это неудивительно, учитывая количество человеческих рассуждений, которые входят в ваш код, что намного сложнее формализовать в сравнении (в этом и состоит исходный Q). Статья WP, например. приходит к полному решению без какого-либо кода, несущему это человеческое рассуждение немного дальше. –

2

Вы

convert([A,B,C,D]) => convert([A,B,C])*10 + D 
=> (convert([A,B])*10+C)*10+D => ... 
=> ((A*10+B)*10+C)*10+D 

Таким образом, вы можете выразить это с помощью простой линейной рекурсией.

Что еще более важно, когда вы выбираете одну возможную цифру от вашего домена 0..9, вы не должны использовать эту цифру больше для последующего выбора:

selectM([A|As],S,Z):- select(A,S,S1),selectM(As,S1,Z). 
selectM([],Z,Z). 

select/3 доступен в SWI Prolog. Вооружившись этим инструментом, вы можете выбрать ваши цифры постепенно от вашего сужая домена :

money_puzzle([[S,E,N,D],[M,O,R,E],[M,O,N,E,Y]]):- 
    Dom = [0,1,2,3,4,5,6,7,8,9], 
    selectM([D,E], Dom,Dom1), add(D,E,0, Y,C1), % D+E=Y 
    selectM([Y,N,R],Dom1,Dom2), add(N,R,C1,E,C2), % N+R=E 
    select( O,  Dom2,Dom3), add(E,O,C2,N,C3), % E+O=N 
    selectM([S,M], Dom3,_),  add(S,M,C3,O,M), % S+M=MO 
    S \== 0, M \== 0. 

Мы можем добавить две цифры с переносом, добавьте производить результирующую цифру с новым переносом (скажем, 4+8 (0) = 2 (1) т.е.12):

add(A,B,C1,D,C2):- N is A+B+C1, D is N mod 10, C2 is N // 10 . 

Таким образом реализован, money_puzzle/1 работает мгновенно, благодаря постепенности, в котором цифры собраны и испытаны сразу:

?- time(money_puzzle(X)). 
% 27,653 inferences, 0.02 CPU in 0.02 seconds (100% CPU, 1380662 Lips) 
X = [[9, 5, 6, 7], [1, 0, 8, 5], [1, 0, 6, 5, 2]] ; 
No 
?- time((money_puzzle(X),fail)). 
% 38,601 inferences, 0.02 CPU in 0.02 seconds (100% CPU, 1927275 Lips) 

задача становится в настоящее время, чтобы сделать его родовое ,

+0

s (X) и вызов принят! – CapelliC

2

Вот мой пример. Я использую , , и mapfoldl/5:

:- meta_predicate mapfoldl(4,?,?,?,?). 
mapfoldl(P_4,Xs,Zs, S0,S) :- 
    list_mapfoldl_(Xs,Zs, S0,S, P_4). 

:- meta_predicate list_mapfoldl_(?,?,?,?,4). 
list_mapfoldl_([],[], S,S, _). 
list_mapfoldl_([X|Xs],[Y|Ys], S0,S, P_4) :- 
    call(P_4,X,Y,S0,S1), 
    list_mapfoldl_(Xs,Ys, S1,S, P_4). 

Давайте соберем mapfoldl/5 хорошее применение и сделать некоторые словесные арифметики!

:- use_module(library(clpfd)). 
:- use_module(library(lambda)). 

digits_number(Ds,Z) :- 
    Ds = [D0|_], 
    Ds ins 0..9, 
    D0 #\= 0,   % most-significant digit must not equal 0 
    reverse(Ds,Rs), 
    length(Ds,N), 
    numlist(1,N,Es), % exponents (+1) 
    maplist(\E1^V^(V is 10**(E1-1)),Es,Ps), 
    scalar_product(Ps,Rs,#=,Z). 

list([]) --> []. 
list([E|Es]) --> [E], list(Es). 

cryptarithexpr_value([V|Vs],X) --> 
    { digits_number([V|Vs],X) }, 
    list([V|Vs]). 
cryptarithexpr_value(T0,T) --> 
    { functor(T0,F,A) }, 
    { dif(F-A,'.'-2) }, 
    { T0 =.. [F|Args0] }, 
    mapfoldl(cryptarithexpr_value,Args0,Args), 
    { T =.. [F|Args] }. 

crypt_arith_(Expr,Zs) :- 
    phrase(cryptarithexpr_value(Expr,Goal),Zs0), 
    ( member(Z,Zs0), \+var(Z) 
    -> throw(error(uninstantiation_error(Expr),crypt_arith_/2)) 
    ; true 
    ), 
    sort(Zs0,Zs), 
    all_different(Zs), 
    call(Goal). 

Быстрый и грязный хак, чтобы сбросить все решения найдено:

solve_n_dump(Opts,Eq) :- 
    ( crypt_arith_(Eq,Zs), 
     labeling(Opts,Zs), 
     format('Eq = (~q), Zs = ~q.~n',[Eq,Zs]), 
     false 
    ; true 
    ). 

solve_n_dump(Eq) :- solve_n_dump([],Eq). 

Давайте попробуем!

 
?- solve_n_dump([S,E,N,D]+[M,O,R,E] #= [M,O,N,E,Y]). 
Eq = ([9,5,6,7]+[1,0,8,5]#=[1,0,6,5,2]), Zs = [9,5,6,7,1,0,8,2]. 
true. 

?- solve_n_dump([C,R,O,S,S]+[R,O,A,D,S] #= [D,A,N,G,E,R]). 
Eq = ([9,6,2,3,3]+[6,2,5,1,3]#=[1,5,8,7,4,6]), Zs = [9,6,2,3,5,1,8,7,4]. 
true. 

?- solve_n_dump([F,O,R,T,Y]+[T,E,N]+[T,E,N] #= [S,I,X,T,Y]). 
Eq = ([2,9,7,8,6]+[8,5,0]+[8,5,0]#=[3,1,4,8,6]), Zs = [2,9,7,8,6,5,0,3,1,4]. 
true. 

?- solve_n_dump([E,A,U]*[E,A,U] #= [O,C,E,A,N]). 
Eq = ([2,0,3]*[2,0,3]#=[4,1,2,0,9]), Zs = [2,0,3,4,1,9]. 
true. 

?- solve_n_dump([N,U,M,B,E,R] #= 3*[P,R,I,M,E]). 
% same as:  [N,U,M,B,E,R] #= [P,R,I,M,E]+[P,R,I,M,E]+[P,R,I,M,E] 
Eq = (3*[5,4,3,2,8]#=[1,6,2,9,8,4]), Zs = [5,4,3,2,8,1,6,9]. 
true. 

?- solve_n_dump(3*[C,O,F,F,E,E] #= [T,H,E,O,R,E,M]). 
Eq = (3*[8,3,1,1,9,9]#=[2,4,9,3,5,9,7]), Zs = [8,3,1,9,2,4,5,7]. 
true. 

Давайте еще немного и попробовать разные labeling options:

 
?- time(solve_n_dump([],[D,O,N,A,L,D]+[G,E,R,A,L,D] #= [R,O,B,E,R,T])). 
Eq = ([5,2,6,4,8,5]+[1,9,7,4,8,5]#=[7,2,3,9,7,0]), Zs = [5,2,6,4,8,1,9,7,3,0]. 
% 35,696,801 inferences, 3.929 CPU in 3.928 seconds (100% CPU, 9085480 Lips) 
true. 

?- time(solve_n_dump([ff],[D,O,N,A,L,D]+[G,E,R,A,L,D] #= [R,O,B,E,R,T])). 
Eq = ([5,2,6,4,8,5]+[1,9,7,4,8,5]#=[7,2,3,9,7,0]), Zs = [5,2,6,4,8,1,9,7,3,0]. 
% 2,902,871 inferences, 0.340 CPU in 0.340 seconds (100% CPU, 8533271 Lips) 
true. 
1

Будет Ness стиль, обобщенную (но предполагая length(A) <= length(B)) решатель:

money_puzzle([A,B,C]) :- 
    maplist(reverse, [A,B,C], [X,Y,Z]), 
    numlist(0, 9, Dom), 
    swc(0, Dom, X,Y,Z), 
    A \= [0|_], B \= [0|_]. 

swc(C, D0, [X|Xs], [Y|Ys], [Z|Zs]) :- 
    peek(D0, X, D1), 
    peek(D1, Y, D2), 
    peek(D2, Z, D3), 
    S is X+Y+C, 
    (S > 9 -> Z is S - 10, C1 = 1 ; Z = S, C1 = 0), 
    swc(C1, D3, Xs, Ys, Zs). 
swc(C, D0, [], [Y|Ys], [Z|Zs]) :- 
    peek(D0, Y, D1), 
    peek(D1, Z, D2), 
    S is Y+C, 
    (S > 9 -> Z is S - 10, C1 = 1 ; Z = S, C1 = 0), 
    swc(C1, D2, [], Ys, Zs). 
swc(0, _, [], [], []). 
swc(1, _, [], [], [1]). 

peek(D, V, R) :- var(V) -> select(V, D, R) ; R = D. 

производительность:

?- time(money_puzzle([S,E,N,D],[M,O,R,E],[M,O,N,E,Y])). 
% 38,710 inferences, 0.016 CPU in 0.016 seconds (100% CPU, 2356481 Lips) 
S = 9, 
E = 5, 
N = 6, 
D = 7, 
M = 1, 
O = 0, 
R = 8, 
Y = 2 ; 
% 15,287 inferences, 0.009 CPU in 0.009 seconds (99% CPU, 1685686 Lips) 
false. 

?- time(money_puzzle([D,O,N,A,L,D],[G,E,R,A,L,D],[R,O,B,E,R,T])). 
% 14,526 inferences, 0.008 CPU in 0.008 seconds (99% CPU, 1870213 Lips) 
D = 5, 
O = 2, 
N = 6, 
A = 4, 
L = 8, 
G = 1, 
E = 9, 
R = 7, 
B = 3, 
T = 0 ; 
% 13,788 inferences, 0.009 CPU in 0.009 seconds (99% CPU, 1486159 Lips) 
false. 
Смежные вопросы