2012-06-07 26 views
7

Ich habe bereits einen funktionierenden verallgemeinerten verbal arithmetic Löser in Prolog, aber es ist zu langsam. Es dauert 8 Minuten, nur um den einfachen Ausdruck auszuführen. S E N D + M O R E = M O N E Y. Kann mir jemand helfen, damit es schneller läuft?Schnellere Implementierung der verbalen Arithmetik in 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. 

Antwort

3

Hinweis: In dieser Antwort wird ein Algorithmus zur Reduzierung der Anzahl der Kombinationen beschrieben, die getestet werden müssen. Ich kenne Prolog nicht, daher kann ich keine Code-Schnipsel bereitstellen.

Der Trick, um eine Brute-Force-Lösung zu beschleunigen, ist Shortcuts. Wenn Sie eine Reihe von Kombinationen identifizieren können, die ungültig sind, können Sie die Anzahl der Kombinationen erheblich reduzieren.

Nehmen Sie das Beispiel in der Hand. Wenn ein Mensch es löst, merkt sie sofort, dass MONEY 5 Ziffern hat, während SEND und MORE nur 4 haben, also muss das M in MONEY die Ziffer 1 sein. 90% der Kombinationen sind weg!

Beim Erstellen eines Algorithmus für einen Computer versuchen wir, Verknüpfungen zu verwenden, die zuerst für alle möglichen Eingaben gelten. Wenn sie nicht die erforderliche Leistung erbringen, beginnen wir mit der Suche nach Verknüpfungen, die nur für bestimmte Kombinationen von Eingaben gelten. Also lassen wir die M = 1 Abkürzung für jetzt.

Stattdessen würde ich auf die letzten Ziffern konzentrieren. Wir wissen, dass (D + E) mod 10 = Y. Das ist unsere 90% Reduktion in der Anzahl der Kombinationen zu versuchen.

Dieser Schritt sollte die Exazution auf knapp eine Minute bringen.

Was können wir tun, wenn das nicht genug ist? Nächster Schritt: Schauen Sie sich die vorletzte Ziffer an! Wir wissen, dass (N + R + Übertrag von D + E) mod 10 = E.

Da wir alle gültige Kombinationen der letzten Ziffer testen, werden wir für jeden Test wissen, ob der Übertrag 0 oder 1 ist Eine Komplikation (für den Code), die die Anzahl der zu testenden Kombinationen weiter reduziert, ist, dass wir auf Duplikate stoßen werden (ein Buchstabe wird einer Nummer zugeordnet, die bereits einem anderen Buchstaben zugewiesen ist). Wenn wir auf ein Duplikat stoßen, können wir zur nächsten Kombination übergehen, ohne weiter in der Kette zu gehen.

Viel Glück mit Ihrer Aufgabe!

+1

Sehr nettes Denken, +1! Genau das macht die CLP (FD) -Version hinter den Kulissen für Sie. Zum Beispiel, wenn ich frage: '? - Puzzle ([S, E, N, D] + [M, O, R, E] = [M, O, N, E, Y]).', Dann bekomme ich als Variablenbindungen: 'M = 1, O = 0, S = 9 ', so dass 3 Variablen bereits auf konkrete ganze Zahlen festgelegt werden, indem einfach die CLP (FD) -Beschränkungen gepostet werden, die das Puzzle beschreiben. Die Bereiche der verbleibenden Variablen sind ebenfalls reduziert, wie wir aus den Restzielen sehen: "N in 5..8, E in 4..7, R in 2..8, Y in 2..8". Ein abschließender Suchschritt findet die eindeutige Lösung als konkrete Integer-Bindings für alle CLP (FD) -Variablen. – mat

6

Betrachten finite domain constraints zum Beispiel in SWI-Prolog mit:

:- 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. 

Beispiel query:

?- 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

hier Schlechte Leistung ist durch alle möglichen Buchstabenzuweisungen zu bilden, bevor Überprüfung, ob ein durchführbares .

Mein Rat lautet: "Scheitern früh, scheitern oft". Das heißt, so viele Fehler wie möglich in die Zuordnungsschritte schieben, um den Suchbaum zu beschneiden.

Klas Lindbäck macht einige gute Vorschläge. Als eine Verallgemeinerung, wenn zwei Zahlen hinzugefügt werden, ist der Übertrag an jeder Stelle höchstens eins. So kann die Zuordnung einzelner Ziffern zu Buchstaben von links nach rechts unter Berücksichtigung der Möglichkeit eines noch unbestimmten Übertrags an den äußersten rechten Stellen überprüft werden. (Natürlich im letzten "Einheiten" Platz, gibt es keinen Übertrag.)

Es ist viel darüber nachzudenken, weshalb Zwang Logik ist, als Matte schlägt vor (und die Sie bereits mit fd_all_different/1) angeschnitten, ist eine solche Bequemlichkeit.


am: Hier ist ein Prolog-Lösung ohne Einschränkungslogik, mit nur einem Hilfs Prädikat Auslassen/3:

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

, die sowohl ein Element aus einer Liste auswählt und erzeugt die verkürzte Liste ohne diesen Gegenstand.

Hier dann ist der Code für sendMoreMoney/3, die durch Auswertung der Summe sucht von links nach rechts:

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,_). 

Wir erhalten durch die Beobachtung, dass der M zu einem schnellen Start muss der Nicht-Null-Übertrag sein aus die Zahl ganz links addiert sich, also 1, und S muss eine andere Ziffer ungleich null sein. Die Kommentare zeigen Schritte, bei denen zusätzliche Buchstaben basierend auf bereits getroffenen Auswahlen deterministisch zugewiesene Werte sein können.


Added (2): Hier ist ein „allgemeines“ Cryptarithm Löser für zwei Summanden, die nicht die gleiche Länge/Anzahl der „Orte“ haben brauchen. Code für length/2 ist als ziemlich üblich integrierte Prädikat weggelassen, und die Aufnahme von Will Ness, Anrufe zu omit/3 sind durch ersetzt wählen/3 für die Bequemlichkeit der SWI-Prolog Benutzer.

Ich habe das mit Amzi getestet! und SWI-Prolog verwenden diese alphametischen Beispiele from Cryptarithms.com, die zwei Summanden umfassen, von denen jeder eine einzigartige Lösung hat. Ich habe auch ein Beispiel mit einem Dutzend Lösungen, I + AM = BEN, erfunden, um das richtige Backtracking zu testen.

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___). 

Ich denke, das zeigt, dass die Vorteile von links nach rechts Suche/Auswertung kann in einer „generalisierten“ Solver erreicht werden, um die Anzahl von Schlüssen um etwa einen Faktor zwei im Vergleich zu der früheren zunehmenden " maßgeschneiderter "Code.

+0

Ihr 'omit/3' ist SWI-Prologs [' select/3'] (http://www.swi-prolog.org/pldoc/doc_for?object=select/3). Verschiedentlich bekannt als "del/3", "delete/3" usw. Die Verwendung erlaubt die direkte Manipulation von endlichen Domänen (oder "Pools"). Das 'selectM/3'-Prädikat aus meiner Antwort enthält mehrere Aufrufe von 'select/3' in eine, um die Codierung zu vereinfachen und zu beschleunigen. Außerdem verwendet dein Code eine Menge menschlicher Überlegungen. –

+0

@WillNess: Es stimmt, dass SWI-Prolog das (äquivalente) Prädikat als eingebaut hat. Ich habe versucht, den Nutzen der Links-nach-Rechts-Bewertung aufzuzeigen, die wir dank Ihrer Rechts-nach-Links-Version vergleichen können. – hardmath

+0

Also habe ich Ihre Version ausprobiert und es dauerte 533 (676) Schlussfolgerungen/0,00 Sekunden, im Vergleich zu 27,653 (38,601) Schlussfolgerungen/0,02 Sekunden, die meine Version braucht. :) Das ist nicht verwunderlich, wenn man bedenkt, wie viel menschliches Denken in Ihren Code gesteckt wird, was im Vergleich viel schwieriger zu formalisieren ist (worum geht es in dem ursprünglichen Q). Der WP Artikel, z.B. kommt ohne Code zu voller Lösung und trägt diese menschliche Argumentation ein bisschen weiter. –

2

Sie haben

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 

So, können Sie dies mit einer einfachen linearen Rekursion ausdrücken können.

Noch wichtiger ist, wenn Sie eine mögliche Stelle von Ihrer Domain 0..9 wählen, sollten Sie nicht diese Ziffer verwenden mehr für die nachfolgende Auswahl:

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

select/3 in SWI Prolog zur Verfügung steht. Bewaffnet mit diesem Tool können Sie Ihre Ziffern wählen allmählich aus dem so verengen Domain:

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. 

Wir zwei Stellen mit einem Übertrag hinzufügen können, fügen Sie eine resultierende Ziffer mit neuem Übertrag erzeugen (etwa 4+8 (0) = 2 (1) d12):

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

So implementiert, money_puzzle/1 läuft augenblicklich dank der allmählichen Art, in der die Ziffern werden gepflückt und getestet sofort:

?- 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) 

Die Herausforderung besteht nun es generisch machen wird .

+0

s (X) und Herausforderung angenommen! – CapelliC

2

Hier ist meine Sicht darauf. Ich benutze , , und 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). 

die mapfoldl/5 für einen guten Zweck lassen und verbale Arithmetik tun!

:- 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). 

quick and dirty Hack alle Lösungen gefunden dump:

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). 

Lassen Sie uns es versuchen!

 
?- 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. 

der einige mehr und versuchen Lass uns einige verschiedene 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

Wird Stil, generali Ness (aber length(A) <= length(B) vorausgesetzt) ​​Löser:

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. 

Leistung:

?- 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.